Skip to content

Commit 18352fc

Browse files
aeitzmancodyoss
authored andcommitted
google/internal/externalaccount: adding BYOID Metrics
Adds framework for sending BYOID metrics via the x-goog-api-client header on outgoing sts requests. Also adds a header file for getting the current version of GoLang Change-Id: Id5431def96f4cfc03e4ada01d5fb8cac8cfa56a9 GitHub-Last-Rev: c93cd47 GitHub-Pull-Request: #661 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/523595 Reviewed-by: Leo Siracusa <[email protected]> Run-TryBot: Cody Oss <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Cody Oss <[email protected]>
1 parent 9095a51 commit 18352fc

13 files changed

+202
-2
lines changed

google/internal/externalaccount/aws.go

+4
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ func shouldUseMetadataServer() bool {
296296
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
297297
}
298298

299+
func (cs awsCredentialSource) credentialSourceType() string {
300+
return "aws"
301+
}
302+
299303
func (cs awsCredentialSource) subjectToken() (string, error) {
300304
if cs.requestSigner == nil {
301305
headers := make(map[string]string)

google/internal/externalaccount/aws_test.go

+17
Original file line numberDiff line numberDiff line change
@@ -1234,3 +1234,20 @@ func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testin
12341234
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
12351235
}
12361236
}
1237+
1238+
func TestAwsCredential_CredentialSourceType(t *testing.T) {
1239+
server := createDefaultAwsTestServer()
1240+
ts := httptest.NewServer(server)
1241+
1242+
tfc := testFileConfig
1243+
tfc.CredentialSource = server.getCredentialSource(ts.URL)
1244+
1245+
base, err := tfc.parse(context.Background())
1246+
if err != nil {
1247+
t.Fatalf("parse() failed %v", err)
1248+
}
1249+
1250+
if got, want := base.credentialSourceType(), "aws"; got != want {
1251+
t.Errorf("got %v but want %v", got, want)
1252+
}
1253+
}

google/internal/externalaccount/basecredentials.go

+11
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
198198
}
199199

200200
type baseCredentialSource interface {
201+
credentialSourceType() string
201202
subjectToken() (string, error)
202203
}
203204

@@ -207,6 +208,15 @@ type tokenSource struct {
207208
conf *Config
208209
}
209210

211+
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
212+
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
213+
goVersion(),
214+
"unknown",
215+
credSource.credentialSourceType(),
216+
conf.ServiceAccountImpersonationURL != "",
217+
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
218+
}
219+
210220
// Token allows tokenSource to conform to the oauth2.TokenSource interface.
211221
func (ts tokenSource) Token() (*oauth2.Token, error) {
212222
conf := ts.conf
@@ -230,6 +240,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
230240
}
231241
header := make(http.Header)
232242
header.Add("Content-Type", "application/x-www-form-urlencoded")
243+
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
233244
clientAuth := clientAuthentication{
234245
AuthStyle: oauth2.AuthStyleInHeader,
235246
ClientID: conf.ClientID,

google/internal/externalaccount/basecredentials_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package externalaccount
66

77
import (
88
"context"
9+
"fmt"
910
"io/ioutil"
1011
"net/http"
1112
"net/http/httptest"
@@ -51,6 +52,7 @@ type testExchangeTokenServer struct {
5152
url string
5253
authorization string
5354
contentType string
55+
metricsHeader string
5456
body string
5557
response string
5658
}
@@ -68,6 +70,10 @@ func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.T
6870
if got, want := headerContentType, tets.contentType; got != want {
6971
t.Errorf("got %v but want %v", got, want)
7072
}
73+
headerMetrics := r.Header.Get("x-goog-api-client")
74+
if got, want := headerMetrics, tets.metricsHeader; got != want {
75+
t.Errorf("got %v but want %v", got, want)
76+
}
7177
body, err := ioutil.ReadAll(r.Body)
7278
if err != nil {
7379
t.Fatalf("Failed reading request body: %s.", err)
@@ -106,6 +112,10 @@ func validateToken(t *testing.T, tok *oauth2.Token) {
106112
}
107113
}
108114

115+
func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
116+
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
117+
}
118+
109119
func TestToken(t *testing.T) {
110120
config := Config{
111121
Audience: "32555940559.apps.googleusercontent.com",
@@ -120,6 +130,7 @@ func TestToken(t *testing.T) {
120130
url: "/",
121131
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
122132
contentType: "application/x-www-form-urlencoded",
133+
metricsHeader: getExpectedMetricsHeader("file", false, false),
123134
body: baseCredsRequestBody,
124135
response: baseCredsResponseBody,
125136
}
@@ -147,6 +158,7 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) {
147158
url: "/",
148159
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
149160
contentType: "application/x-www-form-urlencoded",
161+
metricsHeader: getExpectedMetricsHeader("file", false, false),
150162
body: workforcePoolRequestBodyWithClientId,
151163
response: baseCredsResponseBody,
152164
}
@@ -173,6 +185,7 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
173185
url: "/",
174186
authorization: "",
175187
contentType: "application/x-www-form-urlencoded",
188+
metricsHeader: getExpectedMetricsHeader("file", false, false),
176189
body: workforcePoolRequestBodyWithoutClientId,
177190
response: baseCredsResponseBody,
178191
}

google/internal/externalaccount/executablecredsource.go

+4
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ func (cs executableCredentialSource) parseSubjectTokenFromSource(response []byte
233233
return "", tokenTypeError(source)
234234
}
235235

236+
func (cs executableCredentialSource) credentialSourceType() string {
237+
return "executable"
238+
}
239+
236240
func (cs executableCredentialSource) subjectToken() (string, error) {
237241
if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
238242
return token, err

google/internal/externalaccount/executablecredsource_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ func TestCreateExecutableCredential(t *testing.T) {
150150
if ecs.Timeout != tt.expectedTimeout {
151151
t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout)
152152
}
153+
if ecs.credentialSourceType() != "executable" {
154+
t.Errorf("ecs.CredentialSourceType() got %s but want executable", ecs.credentialSourceType())
155+
}
153156
}
154157
})
155158
}

google/internal/externalaccount/filecredsource.go

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ type fileCredentialSource struct {
1919
Format format
2020
}
2121

22+
func (cs fileCredentialSource) credentialSourceType() string {
23+
return "file"
24+
}
25+
2226
func (cs fileCredentialSource) subjectToken() (string, error) {
2327
tokenFile, err := os.Open(cs.File)
2428
if err != nil {

google/internal/externalaccount/filecredsource_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
6868
t.Errorf("got %v but want %v", out, test.want)
6969
}
7070

71+
if got, want := base.credentialSourceType(), "file"; got != want {
72+
t.Errorf("got %v but want %v", got, want)
73+
}
7174
})
7275
}
7376
}
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package externalaccount
6+
7+
import (
8+
"runtime"
9+
"strings"
10+
"unicode"
11+
)
12+
13+
var (
14+
// version is a package internal global variable for testing purposes.
15+
version = runtime.Version
16+
)
17+
18+
// versionUnknown is only used when the runtime version cannot be determined.
19+
const versionUnknown = "UNKNOWN"
20+
21+
// goVersion returns a Go runtime version derived from the runtime environment
22+
// that is modified to be suitable for reporting in a header, meaning it has no
23+
// whitespace. If it is unable to determine the Go runtime version, it returns
24+
// versionUnknown.
25+
func goVersion() string {
26+
const develPrefix = "devel +"
27+
28+
s := version()
29+
if strings.HasPrefix(s, develPrefix) {
30+
s = s[len(develPrefix):]
31+
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
32+
s = s[:p]
33+
}
34+
return s
35+
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
36+
s = s[:p]
37+
}
38+
39+
notSemverRune := func(r rune) bool {
40+
return !strings.ContainsRune("0123456789.", r)
41+
}
42+
43+
if strings.HasPrefix(s, "go1") {
44+
s = s[2:]
45+
var prerelease string
46+
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
47+
s, prerelease = s[:p], s[p:]
48+
}
49+
if strings.HasSuffix(s, ".") {
50+
s += "0"
51+
} else if strings.Count(s, ".") < 2 {
52+
s += ".0"
53+
}
54+
if prerelease != "" {
55+
// Some release candidates already have a dash in them.
56+
if !strings.HasPrefix(prerelease, "-") {
57+
prerelease = "-" + prerelease
58+
}
59+
s += prerelease
60+
}
61+
return s
62+
}
63+
return "UNKNOWN"
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package externalaccount
6+
7+
import (
8+
"runtime"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
)
13+
14+
func TestGoVersion(t *testing.T) {
15+
testVersion := func(v string) func() string {
16+
return func() string {
17+
return v
18+
}
19+
}
20+
for _, tst := range []struct {
21+
v func() string
22+
want string
23+
}{
24+
{
25+
testVersion("go1.19"),
26+
"1.19.0",
27+
},
28+
{
29+
testVersion("go1.21-20230317-RC01"),
30+
"1.21.0-20230317-RC01",
31+
},
32+
{
33+
testVersion("devel +abc1234"),
34+
"abc1234",
35+
},
36+
{
37+
testVersion("this should be unknown"),
38+
versionUnknown,
39+
},
40+
} {
41+
version = tst.v
42+
got := goVersion()
43+
if diff := cmp.Diff(got, tst.want); diff != "" {
44+
t.Errorf("got(-),want(+):\n%s", diff)
45+
}
46+
}
47+
version = runtime.Version
48+
}

google/internal/externalaccount/impersonate_test.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func createImpersonationServer(urlWanted, authWanted, bodyWanted, response strin
4242
}))
4343
}
4444

45-
func createTargetServer(t *testing.T) *httptest.Server {
45+
func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server {
4646
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4747
if got, want := r.URL.String(), "/"; got != want {
4848
t.Errorf("URL.String(): got %v but want %v", got, want)
@@ -55,6 +55,10 @@ func createTargetServer(t *testing.T) *httptest.Server {
5555
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
5656
t.Errorf("got %v but want %v", got, want)
5757
}
58+
headerMetrics := r.Header.Get("x-goog-api-client")
59+
if got, want := headerMetrics, metricsHeaderWanted; got != want {
60+
t.Errorf("got %v but want %v", got, want)
61+
}
5862
body, err := ioutil.ReadAll(r.Body)
5963
if err != nil {
6064
t.Fatalf("Failed reading request body: %v.", err)
@@ -71,6 +75,7 @@ var impersonationTests = []struct {
7175
name string
7276
config Config
7377
expectedImpersonationBody string
78+
expectedMetricsHeader string
7479
}{
7580
{
7681
name: "Base Impersonation",
@@ -84,6 +89,7 @@ var impersonationTests = []struct {
8489
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
8590
},
8691
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
92+
expectedMetricsHeader: getExpectedMetricsHeader("file", true, false),
8793
},
8894
{
8995
name: "With TokenLifetime Set",
@@ -98,6 +104,7 @@ var impersonationTests = []struct {
98104
ServiceAccountImpersonationLifetimeSeconds: 10000,
99105
},
100106
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
107+
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
101108
},
102109
}
103110

@@ -109,7 +116,7 @@ func TestImpersonation(t *testing.T) {
109116
defer impersonateServer.Close()
110117
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
111118

112-
targetServer := createTargetServer(t)
119+
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
113120
defer targetServer.Close()
114121
testImpersonateConfig.TokenURL = targetServer.URL
115122

google/internal/externalaccount/urlcredsource.go

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type urlCredentialSource struct {
2323
ctx context.Context
2424
}
2525

26+
func (cs urlCredentialSource) credentialSourceType() string {
27+
return "url"
28+
}
29+
2630
func (cs urlCredentialSource) subjectToken() (string, error) {
2731
client := oauth2.NewClient(cs.ctx, nil)
2832
req, err := http.NewRequest("GET", cs.URL, nil)

google/internal/externalaccount/urlcredsource_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,21 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
111111
t.Errorf("got %v but want %v", out, myURLToken)
112112
}
113113
}
114+
115+
func TestURLCredential_CredentialSourceType(t *testing.T) {
116+
cs := CredentialSource{
117+
URL: "http://example.com",
118+
Format: format{Type: fileTypeText},
119+
}
120+
tfc := testFileConfig
121+
tfc.CredentialSource = cs
122+
123+
base, err := tfc.parse(context.Background())
124+
if err != nil {
125+
t.Fatalf("parse() failed %v", err)
126+
}
127+
128+
if got, want := base.credentialSourceType(), "url"; got != want {
129+
t.Errorf("got %v but want %v", got, want)
130+
}
131+
}

0 commit comments

Comments
 (0)