option: support system parameters

- Support system parameters in both gRPC and Http
- For HTTP, we added an http.transport wrapper named as parameterTransport
  to inject the parameters into HTTP headers.
- For gRPC, we added an implementation of credentials.PerRPCCredentials which
  embeds grpc.TokenSource to inject the parameters into the request metadata.
- X-Goog-IAM-Authorization-Token is not supported since it is not
implemented yet at the server-side.
- For more information please read: https://cloud.google.com/apis/docs/system-parameters

Change-Id: I8a6dc00bb7dcd43cb6ac1899d1f55847264dc019
Reviewed-on: https://code-review.googlesource.com/c/37870
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
Reviewed-by: Chris Broadfoot <cbro@google.com>
diff --git a/internal/settings.go b/internal/settings.go
index 92ccf7d..062301c 100644
--- a/internal/settings.go
+++ b/internal/settings.go
@@ -40,6 +40,11 @@
 	GRPCDialOpts    []grpc.DialOption
 	GRPCConn        *grpc.ClientConn
 	NoAuth          bool
+
+	// Google API system parameters. For more information please read:
+	// https://cloud.google.com/apis/docs/system-parameters
+	QuotaProject  string
+	RequestReason string
 }
 
 // Validate reports an error if ds is invalid.
@@ -80,6 +85,12 @@
 	if ds.HTTPClient != nil && ds.GRPCDialOpts != nil {
 		return errors.New("WithHTTPClient is incompatible with gRPC dial options")
 	}
+	if ds.HTTPClient != nil && ds.QuotaProject != "" {
+		return errors.New("WithHTTPClient is incompatible with QuotaProject")
+	}
+	if ds.HTTPClient != nil && ds.RequestReason != "" {
+		return errors.New("WithHTTPClient is incompatible with RequestReason")
+	}
 
 	return nil
 }
diff --git a/internal/settings_test.go b/internal/settings_test.go
index 1522d4c..6f445dd 100644
--- a/internal/settings_test.go
+++ b/internal/settings_test.go
@@ -62,6 +62,8 @@
 		{HTTPClient: &http.Client{}, GRPCConn: &grpc.ClientConn{}},
 		{HTTPClient: &http.Client{}, GRPCDialOpts: []grpc.DialOption{grpc.WithInsecure()}},
 		{Audiences: []string{"foo"}, Scopes: []string{"foo"}},
+		{HTTPClient: &http.Client{}, QuotaProject: "foo"},
+		{HTTPClient: &http.Client{}, RequestReason: "foo"},
 	} {
 		err := ds.Validate()
 		if err == nil {
diff --git a/option/option.go b/option/option.go
index 1e3ace0..0a1c2db 100644
--- a/option/option.go
+++ b/option/option.go
@@ -202,3 +202,34 @@
 type withoutAuthentication struct{}
 
 func (w withoutAuthentication) Apply(o *internal.DialSettings) { o.NoAuth = true }
+
+// WithQuotaProject returns a ClientOption that specifies the project used
+// for quota and billing purposes.
+//
+// For more information please read:
+// https://cloud.google.com/apis/docs/system-parameters
+func WithQuotaProject(quotaProject string) ClientOption {
+	return withQuotaProject(quotaProject)
+}
+
+type withQuotaProject string
+
+func (w withQuotaProject) Apply(o *internal.DialSettings) {
+	o.QuotaProject = string(w)
+}
+
+// WithRequestReason returns a ClientOption that specifies a reason for
+// making the request, which is intended to be recorded in audit logging.
+// An example reason would be a support-case ticket number.
+//
+// For more information please read:
+// https://cloud.google.com/apis/docs/system-parameters
+func WithRequestReason(requestReason string) ClientOption {
+	return withRequestReason(requestReason)
+}
+
+type withRequestReason string
+
+func (w withRequestReason) Apply(o *internal.DialSettings) {
+	o.RequestReason = string(w)
+}
diff --git a/option/option_test.go b/option/option_test.go
index 7b49d91..74f013e 100644
--- a/option/option_test.go
+++ b/option/option_test.go
@@ -52,6 +52,8 @@
 		WithCredentials(&google.DefaultCredentials{ProjectID: "p"}),
 		WithAPIKey("api-key"),
 		WithAudiences("https://example.com/"),
+		WithQuotaProject("user-project"),
+		WithRequestReason("Request Reason"),
 	}
 	var got internal.DialSettings
 	for _, opt := range opts {
@@ -67,6 +69,8 @@
 		CredentialsJSON: []byte(`{some: "json"}`),
 		APIKey:          "api-key",
 		Audiences:       []string{"https://example.com/"},
+		QuotaProject:    "user-project",
+		RequestReason:   "Request Reason",
 	}
 	if !cmp.Equal(got, want, cmpopts.IgnoreUnexported(grpc.ClientConn{})) {
 		t.Errorf("\ngot  %#v\nwant %#v", got, want)
diff --git a/transport/grpc/dial.go b/transport/grpc/dial.go
index 62360f6..4f6b94d 100644
--- a/transport/grpc/dial.go
+++ b/transport/grpc/dial.go
@@ -74,7 +74,11 @@
 			return nil, err
 		}
 		grpcOpts = []grpc.DialOption{
-			grpc.WithPerRPCCredentials(oauth.TokenSource{creds.TokenSource}),
+			grpc.WithPerRPCCredentials(grpcTokenSource{
+				TokenSource:   oauth.TokenSource{creds.TokenSource},
+				quotaProject:  o.QuotaProject,
+				requestReason: o.RequestReason,
+			}),
 			grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
 		}
 	}
@@ -96,3 +100,30 @@
 func addOCStatsHandler(opts []grpc.DialOption) []grpc.DialOption {
 	return append(opts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
 }
+
+// grpcTokenSource supplies PerRPCCredentials from an oauth.TokenSource.
+type grpcTokenSource struct {
+	oauth.TokenSource
+
+	// Additional metadata attached as headers.
+	quotaProject  string
+	requestReason string
+}
+
+// GetRequestMetadata gets the request metadata as a map from a grpcTokenSource.
+func (ts grpcTokenSource) GetRequestMetadata(ctx context.Context, uri ...string) (
+	map[string]string, error) {
+	metadata, err := ts.TokenSource.GetRequestMetadata(ctx, uri...)
+	if err != nil {
+		return nil, err
+	}
+
+	// Attach system parameters into the metadata
+	if ts.quotaProject != "" {
+		metadata["X-goog-user-project"] = ts.quotaProject
+	}
+	if ts.requestReason != "" {
+		metadata["X-goog-request-reason"] = ts.requestReason
+	}
+	return metadata, nil
+}
diff --git a/transport/http/dial.go b/transport/http/dial.go
index a25da67..c0d8bf2 100644
--- a/transport/http/dial.go
+++ b/transport/http/dial.go
@@ -64,9 +64,11 @@
 
 func newTransport(ctx context.Context, base http.RoundTripper, settings *internal.DialSettings) (http.RoundTripper, error) {
 	trans := base
-	trans = userAgentTransport{
-		base:      trans,
-		userAgent: settings.UserAgent,
+	trans = parameterTransport{
+		base:          trans,
+		userAgent:     settings.UserAgent,
+		quotaProject:  settings.QuotaProject,
+		requestReason: settings.RequestReason,
 	}
 	trans = addOCTransport(trans)
 	switch {
@@ -104,12 +106,15 @@
 	return &o, nil
 }
 
-type userAgentTransport struct {
-	userAgent string
-	base      http.RoundTripper
+type parameterTransport struct {
+	userAgent     string
+	quotaProject  string
+	requestReason string
+
+	base http.RoundTripper
 }
 
-func (t userAgentTransport) 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")
@@ -123,7 +128,16 @@
 		newReq.Header[k] = vv
 	}
 	// TODO(cbro): append to existing User-Agent header?
-	newReq.Header["User-Agent"] = []string{t.userAgent}
+	newReq.Header.Set("User-Agent", t.userAgent)
+
+	// Attach system parameters into the header
+	if t.quotaProject != "" {
+		newReq.Header.Set("X-Goog-User-Project", t.quotaProject)
+	}
+	if t.requestReason != "" {
+		newReq.Header.Set("X-Goog-Request-Reason", t.requestReason)
+	}
+
 	return rt.RoundTrip(&newReq)
 }