Added BYOID Metrics

This commit is contained in:
aeitzman 2023-08-28 08:11:33 -07:00
parent a835fc4358
commit 2f8ab1bf0a
13 changed files with 208 additions and 2 deletions

View File

@ -339,6 +339,10 @@ func shouldUseMetadataServer() bool {
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment() return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
} }
func (cs awsCredentialSource) credentialSourceType() string {
return "aws"
}
func (cs awsCredentialSource) subjectToken() (string, error) { func (cs awsCredentialSource) subjectToken() (string, error) {
if cs.requestSigner == nil { if cs.requestSigner == nil {
headers := make(map[string]string) headers := make(map[string]string)

View File

@ -1442,3 +1442,30 @@ func TestAWSCredential_Validations(t *testing.T) {
}) })
} }
} }
func TestAwsCredential_CredentialSourceType(t *testing.T) {
server := createDefaultAwsTestServer()
ts := httptest.NewServer(server)
tsURL, err := neturl.Parse(ts.URL)
if err != nil {
t.Fatalf("couldn't parse httptest servername")
}
oldValidHostnames := validHostnames
defer func() {
validHostnames = oldValidHostnames
}()
validHostnames = []string{tsURL.Hostname()}
tfc := testFileConfig
tfc.CredentialSource = server.getCredentialSource(ts.URL)
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
if got, want := base.credentialSourceType(), "aws"; got != want {
t.Errorf("got %v but want %v", got, want)
}
}

View File

@ -202,6 +202,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
} }
type baseCredentialSource interface { type baseCredentialSource interface {
credentialSourceType() string
subjectToken() (string, error) subjectToken() (string, error)
} }
@ -211,6 +212,15 @@ type tokenSource struct {
conf *Config conf *Config
} }
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
goVersion(),
"unknown",
credSource.credentialSourceType(),
conf.ServiceAccountImpersonationURL != "",
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
}
// Token allows tokenSource to conform to the oauth2.TokenSource interface. // Token allows tokenSource to conform to the oauth2.TokenSource interface.
func (ts tokenSource) Token() (*oauth2.Token, error) { func (ts tokenSource) Token() (*oauth2.Token, error) {
conf := ts.conf conf := ts.conf
@ -234,6 +244,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
} }
header := make(http.Header) header := make(http.Header)
header.Add("Content-Type", "application/x-www-form-urlencoded") header.Add("Content-Type", "application/x-www-form-urlencoded")
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
clientAuth := clientAuthentication{ clientAuth := clientAuthentication{
AuthStyle: oauth2.AuthStyleInHeader, AuthStyle: oauth2.AuthStyleInHeader,
ClientID: conf.ClientID, ClientID: conf.ClientID,

View File

@ -6,6 +6,7 @@ package externalaccount
import ( import (
"context" "context"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -51,6 +52,7 @@ type testExchangeTokenServer struct {
url string url string
authorization string authorization string
contentType string contentType string
metricsHeader string
body string body string
response string response string
} }
@ -68,6 +70,10 @@ func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.T
if got, want := headerContentType, tets.contentType; got != want { if got, want := headerContentType, tets.contentType; got != want {
t.Errorf("got %v but want %v", got, want) t.Errorf("got %v but want %v", got, want)
} }
headerMetrics := r.Header.Get("x-goog-api-client")
if got, want := headerMetrics, tets.metricsHeader; got != want {
t.Errorf("got %v but want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
t.Fatalf("Failed reading request body: %s.", err) t.Fatalf("Failed reading request body: %s.", err)
@ -106,6 +112,10 @@ func validateToken(t *testing.T, tok *oauth2.Token) {
} }
} }
func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
}
func TestToken(t *testing.T) { func TestToken(t *testing.T) {
config := Config{ config := Config{
Audience: "32555940559.apps.googleusercontent.com", Audience: "32555940559.apps.googleusercontent.com",
@ -120,6 +130,7 @@ func TestToken(t *testing.T) {
url: "/", url: "/",
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: baseCredsRequestBody, body: baseCredsRequestBody,
response: baseCredsResponseBody, response: baseCredsResponseBody,
} }
@ -147,6 +158,7 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) {
url: "/", url: "/",
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=", authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: workforcePoolRequestBodyWithClientId, body: workforcePoolRequestBodyWithClientId,
response: baseCredsResponseBody, response: baseCredsResponseBody,
} }
@ -173,6 +185,7 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
url: "/", url: "/",
authorization: "", authorization: "",
contentType: "application/x-www-form-urlencoded", contentType: "application/x-www-form-urlencoded",
metricsHeader: getExpectedMetricsHeader("file", false, false),
body: workforcePoolRequestBodyWithoutClientId, body: workforcePoolRequestBodyWithoutClientId,
response: baseCredsResponseBody, response: baseCredsResponseBody,
} }

View File

@ -233,6 +233,10 @@ func (cs executableCredentialSource) parseSubjectTokenFromSource(response []byte
return "", tokenTypeError(source) return "", tokenTypeError(source)
} }
func (cs executableCredentialSource) credentialSourceType() string {
return "executable"
}
func (cs executableCredentialSource) subjectToken() (string, error) { func (cs executableCredentialSource) subjectToken() (string, error) {
if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil { if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
return token, err return token, err

View File

@ -150,6 +150,9 @@ func TestCreateExecutableCredential(t *testing.T) {
if ecs.Timeout != tt.expectedTimeout { if ecs.Timeout != tt.expectedTimeout {
t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout) t.Errorf("ecs.Timeout got %v but want %v", ecs.Timeout, tt.expectedTimeout)
} }
if ecs.credentialSourceType() != "executable" {
t.Errorf("ecs.CredentialSourceType() got %s but want executable", ecs.credentialSourceType())
}
} }
}) })
} }

View File

@ -19,6 +19,10 @@ type fileCredentialSource struct {
Format format Format format
} }
func (cs fileCredentialSource) credentialSourceType() string {
return "file"
}
func (cs fileCredentialSource) subjectToken() (string, error) { func (cs fileCredentialSource) subjectToken() (string, error) {
tokenFile, err := os.Open(cs.File) tokenFile, err := os.Open(cs.File)
if err != nil { if err != nil {

View File

@ -68,6 +68,9 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
t.Errorf("got %v but want %v", out, test.want) t.Errorf("got %v but want %v", out, test.want)
} }
if got, want := base.credentialSourceType(), "file"; got != want {
t.Errorf("got %v but want %v", got, want)
}
}) })
} }
} }

View File

@ -0,0 +1,62 @@
// Copyright 2023 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 (
"runtime"
"strings"
"unicode"
)
var (
// version is a package internal global variable for testing purposes.
version = runtime.Version
)
// versionUnknown is only used when the runtime version cannot be determined.
const versionUnknown = "UNKNOWN"
// goVersion returns a Go runtime version derived from the runtime environment
// that is modified to be suitable for reporting in a header, meaning it has no
// whitespace. If it is unable to determine the Go runtime version, it returns
// versionUnknown.
func goVersion() string {
const develPrefix = "devel +"
s := version()
if strings.HasPrefix(s, develPrefix) {
s = s[len(develPrefix):]
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
s = s[:p]
}
return s
}
notSemverRune := func(r rune) bool {
return !strings.ContainsRune("0123456789.", r)
}
if strings.HasPrefix(s, "go1") {
s = s[2:]
var prerelease string
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
s, prerelease = s[:p], s[p:]
}
if strings.HasSuffix(s, ".") {
s += "0"
} else if strings.Count(s, ".") < 2 {
s += ".0"
}
if prerelease != "" {
// Some release candidates already have a dash in them.
if !strings.HasPrefix(prerelease, "-") {
prerelease = "-" + prerelease
}
s += prerelease
}
return s
}
return "UNKNOWN"
}

View File

@ -0,0 +1,46 @@
// Copyright 2023 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 (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestGoVersion(t *testing.T) {
testVersion := func(v string) func() string {
return func() string {
return v
}
}
for _, tst := range []struct {
v func() string
want string
}{
{
testVersion("go1.19"),
"1.19.0",
},
{
testVersion("go1.21-20230317-RC01"),
"1.21.0-20230317-RC01",
},
{
testVersion("devel +abc1234"),
"abc1234",
},
{
testVersion("this should be unknown"),
versionUnknown,
},
} {
version = tst.v
got := goVersion()
if diff := cmp.Diff(got, tst.want); diff != "" {
t.Errorf("got(-),want(+):\n%s", diff)
}
}
}

View File

@ -42,7 +42,7 @@ func createImpersonationServer(urlWanted, authWanted, bodyWanted, response strin
})) }))
} }
func createTargetServer(t *testing.T) *httptest.Server { func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.String(), "/"; got != want { if got, want := r.URL.String(), "/"; got != want {
t.Errorf("URL.String(): got %v but want %v", got, want) t.Errorf("URL.String(): got %v but want %v", got, want)
@ -55,6 +55,10 @@ func createTargetServer(t *testing.T) *httptest.Server {
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want { if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
t.Errorf("got %v but want %v", got, want) t.Errorf("got %v but want %v", got, want)
} }
headerMetrics := r.Header.Get("x-goog-api-client")
if got, want := headerMetrics, metricsHeaderWanted; got != want {
t.Errorf("got %v but want %v", got, want)
}
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
t.Fatalf("Failed reading request body: %v.", err) t.Fatalf("Failed reading request body: %v.", err)
@ -71,6 +75,7 @@ var impersonationTests = []struct {
name string name string
config Config config Config
expectedImpersonationBody string expectedImpersonationBody string
expectedMetricsHeader string
}{ }{
{ {
name: "Base Impersonation", name: "Base Impersonation",
@ -84,6 +89,7 @@ var impersonationTests = []struct {
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"}, Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
}, },
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
expectedMetricsHeader: getExpectedMetricsHeader("file", true, false),
}, },
{ {
name: "With TokenLifetime Set", name: "With TokenLifetime Set",
@ -98,6 +104,7 @@ var impersonationTests = []struct {
ServiceAccountImpersonationLifetimeSeconds: 10000, ServiceAccountImpersonationLifetimeSeconds: 10000,
}, },
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}", expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
}, },
} }
@ -109,7 +116,7 @@ func TestImpersonation(t *testing.T) {
defer impersonateServer.Close() defer impersonateServer.Close()
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
targetServer := createTargetServer(t) targetServer := createTargetServer(tt.expectedMetricsHeader, t)
defer targetServer.Close() defer targetServer.Close()
testImpersonateConfig.TokenURL = targetServer.URL testImpersonateConfig.TokenURL = targetServer.URL

View File

@ -23,6 +23,10 @@ type urlCredentialSource struct {
ctx context.Context ctx context.Context
} }
func (cs urlCredentialSource) credentialSourceType() string {
return "url"
}
func (cs urlCredentialSource) subjectToken() (string, error) { func (cs urlCredentialSource) subjectToken() (string, error) {
client := oauth2.NewClient(cs.ctx, nil) client := oauth2.NewClient(cs.ctx, nil)
req, err := http.NewRequest("GET", cs.URL, nil) req, err := http.NewRequest("GET", cs.URL, nil)

View File

@ -111,3 +111,21 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
t.Errorf("got %v but want %v", out, myURLToken) t.Errorf("got %v but want %v", out, myURLToken)
} }
} }
func TestURLCredential_CredentialSourceType(t *testing.T) {
cs := CredentialSource{
URL: "http://example.com",
Format: format{Type: fileTypeText},
}
tfc := testFileConfig
tfc.CredentialSource = cs
base, err := tfc.parse(context.Background())
if err != nil {
t.Fatalf("parse() failed %v", err)
}
if got, want := base.credentialSourceType(), "url"; got != want {
t.Errorf("got %v but want %v", got, want)
}
}