feat(option): support service account impersonation (#625)

This adds support for service account impersonation with the use
of a new client option. This implementation only supports
service-account/user-credential impersonating another service
account. This does not cover the use-case that involves
impersonating a service account as an admin user with domain wide
delegation.

Fixes: #378
diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go
index 67c8965..be52f3b 100644
--- a/idtoken/idtoken.go
+++ b/idtoken/idtoken.go
@@ -78,6 +78,9 @@
 	if ds.TokenSource != nil {
 		return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported")
 	}
+	if ds.ImpersonationConfig != nil {
+		return nil, fmt.Errorf("idtoken: option.WithImpersonatedCredentials not supported")
+	}
 	return newTokenSource(ctx, audience, &ds)
 }
 
diff --git a/integration-tests/impersonate/impersonate_test.go b/integration-tests/impersonate/impersonate_test.go
new file mode 100644
index 0000000..efbf920
--- /dev/null
+++ b/integration-tests/impersonate/impersonate_test.go
@@ -0,0 +1,91 @@
+// Copyright 2020 Google LLC.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build integration
+
+package impersonate
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+	"os"
+	"testing"
+	"time"
+
+	"google.golang.org/api/option"
+
+	"google.golang.org/api/storage/v1"
+)
+
+var (
+	// envReaderCredentialFile points to a service accountthat is a "Service
+	// Account Token Creator" on envReaderSA.
+	envBaseSACredentialFile = "API_GO_CLIENT_IMPERSONATE_BASE"
+	// envUserCredentialFile points to a user credential that is a "Service
+	// Account Token Creator" on envReaderSA.
+	envUserCredentialFile = "API_GO_CLIENT_IMPERSONATE_USER"
+	// envReaderCredentialFile points to a service account that is a "Storage
+	// Object Reader" and is a "Service Account Token Creator" on envWriterSA.
+	envReaderCredentialFile = "API_GO_CLIENT_IMPERSONATE_READER"
+	// envReaderSA is the name of the reader service account.
+	envReaderSA = "API_GO_CLIENT_IMPERSONATE_READER_SA"
+	// envWriterSA is the name of the writer service account. This service
+	// account has been granted roles/serviceusage.serviceUsageConsumer.
+	envWriterSA = "API_GO_CLIENT_IMPERSONATE_WRITER_SA"
+	// envProjectID is a project that hosts a GCS bucket.
+	envProjectID = "GOOGLE_CLOUD_PROJECT"
+)
+
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
+
+func TestImpersonatedCredentials(t *testing.T) {
+	ctx := context.Background()
+	projID := os.Getenv(envProjectID)
+	writerSA := os.Getenv(envWriterSA)
+	tests := []struct {
+		name           string
+		baseSALocation string
+		delgates       []string
+	}{
+		{
+			name:           "SA -> SA",
+			baseSALocation: os.Getenv(envReaderCredentialFile),
+			delgates:       []string{},
+		},
+		{
+			name:           "SA -> Delegate -> SA",
+			baseSALocation: os.Getenv(envBaseSACredentialFile),
+			delgates:       []string{os.Getenv(envReaderSA)},
+		},
+		{
+			name:           "User Credential -> Delegate -> SA",
+			baseSALocation: os.Getenv(envUserCredentialFile),
+			delgates:       []string{os.Getenv(envReaderSA)},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			svc, err := storage.NewService(ctx,
+				option.WithCredentialsFile(tt.baseSALocation),
+				option.ImpersonateCredentials(writerSA, tt.delgates...),
+			)
+			if err != nil {
+				t.Fatalf("failed to create client: %v", err)
+			}
+			bucketName := fmt.Sprintf("%s-%d", projID, rand.Int63())
+			if _, err := svc.Buckets.Insert(projID, &storage.Bucket{
+				Name: bucketName,
+			}).Do(); err != nil {
+				t.Fatalf("error creating bucket: %v", err)
+			}
+			if err := svc.Buckets.Delete(bucketName).Do(); err != nil {
+				t.Fatalf("unable to cleanup bucket %q: %v", bucketName, err)
+			}
+		})
+	}
+}
diff --git a/internal/creds.go b/internal/creds.go
index 75e9445..dc6d50e 100644
--- a/internal/creds.go
+++ b/internal/creds.go
@@ -11,6 +11,7 @@
 	"io/ioutil"
 
 	"golang.org/x/oauth2"
+	"google.golang.org/api/internal/impersonate"
 
 	"golang.org/x/oauth2/google"
 )
@@ -18,6 +19,17 @@
 // Creds returns credential information obtained from DialSettings, or if none, then
 // it returns default credential information.
 func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
+	creds, err := baseCreds(ctx, ds)
+	if err != nil {
+		return nil, err
+	}
+	if ds.ImpersonationConfig != nil {
+		return impersonateCredentials(ctx, creds, ds)
+	}
+	return creds, nil
+}
+
+func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
 	if ds.Credentials != nil {
 		return ds.Credentials, nil
 	}
@@ -103,3 +115,17 @@
 	}
 	return v.QuotaProject
 }
+
+func impersonateCredentials(ctx context.Context, creds *google.Credentials, ds *DialSettings) (*google.Credentials, error) {
+	if len(ds.ImpersonationConfig.Scopes) == 0 {
+		ds.ImpersonationConfig.Scopes = ds.Scopes
+	}
+	ts, err := impersonate.TokenSource(ctx, creds.TokenSource, ds.ImpersonationConfig)
+	if err != nil {
+		return nil, err
+	}
+	return &google.Credentials{
+		TokenSource: ts,
+		ProjectID:   creds.ProjectID,
+	}, nil
+}
diff --git a/internal/impersonate/impersonate.go b/internal/impersonate/impersonate.go
new file mode 100644
index 0000000..b465bbc
--- /dev/null
+++ b/internal/impersonate/impersonate.go
@@ -0,0 +1,128 @@
+// Copyright 2020 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 impersonate is used to impersonate Google Credentials.
+package impersonate
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"time"
+
+	"golang.org/x/oauth2"
+)
+
+// Config for generating impersonated credentials.
+type Config struct {
+	// Target is the service account to impersonate. Required.
+	Target string
+	// Scopes the impersonated credential should have. Required.
+	Scopes []string
+	// Delegates are the service accounts in a delegation chain. Each service
+	// account must be granted roles/iam.serviceAccountTokenCreator on the next
+	// service account in the chain. Optional.
+	Delegates []string
+}
+
+// TokenSource returns an impersonated TokenSource configured with the provided
+// config using ts as the base credential provider for making requests.
+func TokenSource(ctx context.Context, ts oauth2.TokenSource, config *Config) (oauth2.TokenSource, error) {
+	if len(config.Scopes) == 0 {
+		return nil, fmt.Errorf("impersonate: scopes must be provided")
+	}
+	its := impersonatedTokenSource{
+		ctx:  ctx,
+		ts:   ts,
+		name: formatIAMServiceAccountName(config.Target),
+		// Default to the longest acceptable value of one hour as the token will
+		// be refreshed automatically.
+		lifetime: "3600s",
+	}
+
+	its.delegates = make([]string, len(config.Delegates))
+	for i, v := range config.Delegates {
+		its.delegates[i] = formatIAMServiceAccountName(v)
+	}
+	its.scopes = make([]string, len(config.Scopes))
+	copy(its.scopes, config.Scopes)
+
+	return oauth2.ReuseTokenSource(nil, its), nil
+}
+
+func formatIAMServiceAccountName(name string) string {
+	return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
+}
+
+type generateAccessTokenReq struct {
+	Delegates []string `json:"delegates,omitempty"`
+	Lifetime  string   `json:"lifetime,omitempty"`
+	Scope     []string `json:"scope,omitempty"`
+}
+
+type generateAccessTokenResp struct {
+	AccessToken string `json:"accessToken"`
+	ExpireTime  string `json:"expireTime"`
+}
+
+type impersonatedTokenSource struct {
+	ctx context.Context
+	ts  oauth2.TokenSource
+
+	name      string
+	lifetime  string
+	scopes    []string
+	delegates []string
+}
+
+// Token returns an impersonated Token.
+func (i impersonatedTokenSource) Token() (*oauth2.Token, error) {
+	hc := oauth2.NewClient(i.ctx, i.ts)
+	reqBody := generateAccessTokenReq{
+		Delegates: i.delegates,
+		Lifetime:  i.lifetime,
+		Scope:     i.scopes,
+	}
+	b, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("impersonate: unable to marshal request: %v", err)
+	}
+	url := fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", i.name)
+	req, err := http.NewRequest("POST", url, bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("impersonate: unable to create request: %v", err)
+	}
+	req = req.WithContext(i.ctx)
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := hc.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("impersonate: unable to generate access token: %v", err)
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
+	if err != nil {
+		return nil, fmt.Errorf("impersonate: unable to read body: %v", err)
+	}
+	if c := resp.StatusCode; c < 200 || c > 299 {
+		return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
+	}
+
+	var accessTokenResp generateAccessTokenResp
+	if err := json.Unmarshal(body, &accessTokenResp); err != nil {
+		return nil, fmt.Errorf("impersonate: unable to parse response: %v", err)
+	}
+	expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
+	if err != nil {
+		return nil, fmt.Errorf("impersonate: unable to parse expiry: %v", err)
+	}
+	return &oauth2.Token{
+		AccessToken: accessTokenResp.AccessToken,
+		Expiry:      expiry,
+	}, nil
+}
diff --git a/internal/settings.go b/internal/settings.go
index 3b779c2..26259b8 100644
--- a/internal/settings.go
+++ b/internal/settings.go
@@ -12,6 +12,7 @@
 
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
+	"google.golang.org/api/internal/impersonate"
 	"google.golang.org/grpc"
 )
 
@@ -39,6 +40,7 @@
 	ClientCertSource    func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
 	CustomClaims        map[string]interface{}
 	SkipValidation      bool
+	ImpersonationConfig *impersonate.Config
 
 	// Google API system parameters. For more information please read:
 	// https://cloud.google.com/apis/docs/system-parameters
@@ -105,6 +107,8 @@
 	if ds.ClientCertSource != nil && (ds.GRPCConn != nil || ds.GRPCConnPool != nil || ds.GRPCConnPoolSize != 0 || ds.GRPCDialOpts != nil) {
 		return errors.New("WithClientCertSource is currently only supported for HTTP. gRPC settings are incompatible")
 	}
-
+	if ds.ImpersonationConfig != nil && len(ds.ImpersonationConfig.Scopes) == 0 && len(ds.Scopes) == 0 {
+		return errors.New("WithImpersonatedCredentials requires scopes being provided")
+	}
 	return nil
 }
diff --git a/internal/settings_test.go b/internal/settings_test.go
index 818e15b..8b6e6d3 100644
--- a/internal/settings_test.go
+++ b/internal/settings_test.go
@@ -10,6 +10,7 @@
 	"net/http"
 	"testing"
 
+	"google.golang.org/api/internal/impersonate"
 	"google.golang.org/grpc"
 
 	"golang.org/x/oauth2"
@@ -35,6 +36,8 @@
 		// the check feasible.
 		{NoAuth: true, Scopes: []string{"s"}},
 		{ClientCertSource: dummyGetClientCertificate},
+		{ImpersonationConfig: &impersonate.Config{Scopes: []string{"x"}}},
+		{ImpersonationConfig: &impersonate.Config{}, Scopes: []string{"x"}},
 	} {
 		err := ds.Validate()
 		if err != nil {
@@ -63,6 +66,7 @@
 		{ClientCertSource: dummyGetClientCertificate, GRPCConnPool: struct{ ConnPool }{}},
 		{ClientCertSource: dummyGetClientCertificate, GRPCDialOpts: []grpc.DialOption{grpc.WithInsecure()}},
 		{ClientCertSource: dummyGetClientCertificate, GRPCConnPoolSize: 1},
+		{ImpersonationConfig: &impersonate.Config{}},
 	} {
 		err := ds.Validate()
 		if err == nil {
diff --git a/option/option.go b/option/option.go
index b7c40d6..686476f 100644
--- a/option/option.go
+++ b/option/option.go
@@ -11,6 +11,7 @@
 
 	"golang.org/x/oauth2"
 	"google.golang.org/api/internal"
+	"google.golang.org/api/internal/impersonate"
 	"google.golang.org/grpc"
 )
 
@@ -269,3 +270,57 @@
 func (w withClientCertSource) Apply(o *internal.DialSettings) {
 	o.ClientCertSource = w.s
 }
+
+// ImpersonateCredentials returns a ClientOption that will impersonate the
+// target service account.
+//
+// In order to impersonate the target service account
+// the base service account must have the Service Account Token Creator role,
+// roles/iam.serviceAccountTokenCreator, on the target service account.
+// See https://cloud.google.com/iam/docs/understanding-service-accounts.
+//
+// Optionally, delegates can be used during impersonation if the base service
+// account lacks the token creator role on the target. When using delegates,
+// each service account must be granted roles/iam.serviceAccountTokenCreator
+// on the next service account in the chain.
+//
+// For example, if a base service account of SA1 is trying to impersonate target
+// service account SA2 while using delegate service accounts DSA1 and DSA2,
+// the following must be true:
+//
+//   1. Base service account SA1 has roles/iam.serviceAccountTokenCreator on
+//      DSA1.
+//   2. DSA1 has roles/iam.serviceAccountTokenCreator on DSA2.
+//   3. DSA2 has roles/iam.serviceAccountTokenCreator on target SA2.
+//
+// The resulting impersonated credential will either have the default scopes of
+// the client being instantiating or the scopes from WithScopes if provided.
+// Scopes are required for creating impersonated credentials, so if this option
+// is used while not using a NewClient/NewService function, WithScopes must also
+// be explicitly passed in as well.
+//
+// If the base credential is an authorized user and not a service account, or if
+// the option WithQuotaProject is set, the target service account must have a
+// role that grants the serviceusage.services.use permission such as
+// roles/serviceusage.serviceUsageConsumer.
+//
+// This is an EXPERIMENTAL API and may be changed or removed in the future.
+func ImpersonateCredentials(target string, delegates ...string) ClientOption {
+	return impersonateServiceAccount{
+		target:    target,
+		delegates: delegates,
+	}
+}
+
+type impersonateServiceAccount struct {
+	target    string
+	delegates []string
+}
+
+func (i impersonateServiceAccount) Apply(o *internal.DialSettings) {
+	o.ImpersonationConfig = &impersonate.Config{
+		Target: i.target,
+	}
+	o.ImpersonationConfig.Delegates = make([]string, len(i.delegates))
+	copy(o.ImpersonationConfig.Delegates, i.delegates)
+}