forked from Mirrors/oauth2
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: c93cd478e5
GitHub-Pull-Request: golang/oauth2#661
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/523595
Reviewed-by: Leo Siracusa <leosiracusa@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Cody Oss <codyoss@google.com>
This commit is contained in:
parent
9095a51613
commit
18352fc433
|
@ -296,6 +296,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)
|
||||||
|
|
|
@ -1234,3 +1234,20 @@ func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testin
|
||||||
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
||||||
|
server := createDefaultAwsTestServer()
|
||||||
|
ts := httptest.NewServer(server)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -198,6 +198,7 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type baseCredentialSource interface {
|
type baseCredentialSource interface {
|
||||||
|
credentialSourceType() string
|
||||||
subjectToken() (string, error)
|
subjectToken() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +208,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
|
||||||
|
@ -230,6 +240,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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
// 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
|
||||||
|
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||||
|
s = s[:p]
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// 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"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version = runtime.Version
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue