transport: populate QuotaProject from Creds.JSON["quota_project_id"]

Automatically populates the quota project from the quota_project_id specified
by JSON credentials.

Change-Id: I1e56ed7955fc5e84fae287d3958f7079214d67ee
Reviewed-on: https://code-review.googlesource.com/c/google-api-go-client/+/43150
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
diff --git a/internal/creds.go b/internal/creds.go
index a6f9a2d..75e9445 100644
--- a/internal/creds.go
+++ b/internal/creds.go
@@ -90,3 +90,16 @@
 	}
 	return google.JWTAccessTokenSourceFromJSON(data, audience)
 }
+
+// QuotaProjectFromCreds returns the quota project from the JSON blob in the provided credentials.
+//
+// NOTE(cbro): consider promoting this to a field on google.Credentials.
+func QuotaProjectFromCreds(cred *google.Credentials) string {
+	var v struct {
+		QuotaProject string `json:"quota_project_id"`
+	}
+	if err := json.Unmarshal(cred.JSON, &v); err != nil {
+		return ""
+	}
+	return v.QuotaProject
+}
diff --git a/internal/creds_test.go b/internal/creds_test.go
index 11c7dcc..c80a5df 100644
--- a/internal/creds_test.go
+++ b/internal/creds_test.go
@@ -115,3 +115,29 @@
   "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
   "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dumba-504%40appspot.gserviceaccount.com"
 }`
+
+func TestQuotaProjectFromCreds(t *testing.T) {
+	ctx := context.Background()
+
+	cred, err := credentialsFromJSON(ctx, []byte(validServiceAccountJSON), "foo.googleapis.com", nil, nil)
+	if err != nil {
+		t.Fatalf("got %v, wanted no error", err)
+	}
+	if want, got := "", QuotaProjectFromCreds(cred); want != got {
+		t.Errorf("QuotaProjectFromCreds(validServiceAccountJSON): want %q, got %q", want, got)
+	}
+
+	quotaProjectJSON := []byte(`
+{
+	"type": "authorized_user",
+	"quota_project_id": "foobar"
+}`)
+
+	cred, err = credentialsFromJSON(ctx, []byte(quotaProjectJSON), "foo.googleapis.com", nil, nil)
+	if err != nil {
+		t.Fatalf("got %v, wanted no error", err)
+	}
+	if want, got := "foobar", QuotaProjectFromCreds(cred); want != got {
+		t.Errorf("QuotaProjectFromCreds(quotaProjectJSON): want %q, got %q", want, got)
+	}
+}
diff --git a/transport/grpc/dial.go b/transport/grpc/dial.go
index 7526e68..e823f78 100644
--- a/transport/grpc/dial.go
+++ b/transport/grpc/dial.go
@@ -71,6 +71,11 @@
 		if err != nil {
 			return nil, err
 		}
+
+		if o.QuotaProject == "" {
+			o.QuotaProject = internal.QuotaProjectFromCreds(creds)
+		}
+
 		// Attempt Direct Path only if:
 		// * The endpoint is a host:port (or dns:///host:port).
 		// * Credentials are obtained via GCE metadata server, using the default
diff --git a/transport/http/dial.go b/transport/http/dial.go
index 1ef67ce..c2ca3b5 100644
--- a/transport/http/dial.go
+++ b/transport/http/dial.go
@@ -53,13 +53,13 @@
 }
 
 func newTransport(ctx context.Context, base http.RoundTripper, settings *internal.DialSettings) (http.RoundTripper, error) {
-	trans := base
-	trans = parameterTransport{
-		base:          trans,
+	paramTransport := &parameterTransport{
+		base:          base,
 		userAgent:     settings.UserAgent,
 		quotaProject:  settings.QuotaProject,
 		requestReason: settings.RequestReason,
 	}
+	var trans http.RoundTripper = paramTransport
 	trans = addOCTransport(trans, settings)
 	switch {
 	case settings.NoAuth:
@@ -74,6 +74,9 @@
 		if err != nil {
 			return nil, err
 		}
+		if paramTransport.quotaProject == "" {
+			paramTransport.quotaProject = internal.QuotaProjectFromCreds(creds)
+		}
 		trans = &oauth2.Transport{
 			Base:   trans,
 			Source: creds.TokenSource,
@@ -104,7 +107,7 @@
 	base http.RoundTripper
 }
 
-func (t parameterTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+func (t *parameterTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	rt := t.base
 	if rt == nil {
 		return nil, errors.New("transport: no Transport specified")