google: add support for "impersonated_service_account" credential type.
New credential type supported: "impersonated_service_account".
Extend the "credentialsFile" struct to take into account the credential source for the impersonation.
Reuse of `ImpersonateTokenSource` struct, from `google/internal/externalaccount/Impersonate.go' file. The struct has a package-scope visibility now.
Fixes: #515
Change-Id: I87e213be6d4b6add2d6d82b91b1b38e43a0d2fe4
GitHub-Last-Rev: 14806e6b37a019cbff58e2088ee99191e89b4f7e
GitHub-Pull-Request: golang/oauth2#516
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/344369
Reviewed-by: Cody Oss <codyoss@google.com>
Trust: Cody Oss <codyoss@google.com>
Trust: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/google/google.go b/google/google.go
index 41ced10..ccc23ee 100644
--- a/google/google.go
+++ b/google/google.go
@@ -92,9 +92,10 @@
// JSON key file types.
const (
- serviceAccountKey = "service_account"
- userCredentialsKey = "authorized_user"
- externalAccountKey = "external_account"
+ serviceAccountKey = "service_account"
+ userCredentialsKey = "authorized_user"
+ externalAccountKey = "external_account"
+ impersonatedServiceAccount = "impersonated_service_account"
)
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -121,9 +122,13 @@
TokenURLExternal string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
+ Delegates []string `json:"delegates"`
CredentialSource externalaccount.CredentialSource `json:"credential_source"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
+
+ // Service account impersonation
+ SourceCredentials *credentialsFile `json:"source_credentials"`
}
func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
@@ -180,6 +185,23 @@
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
}
return cfg.TokenSource(ctx)
+ case impersonatedServiceAccount:
+ if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
+ return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
+ }
+
+ ts, err := f.SourceCredentials.tokenSource(ctx, params)
+ if err != nil {
+ return nil, err
+ }
+ imp := externalaccount.ImpersonateTokenSource{
+ Ctx: ctx,
+ URL: f.ServiceAccountImpersonationURL,
+ Scopes: params.Scopes,
+ Ts: ts,
+ Delegates: f.Delegates,
+ }
+ return oauth2.ReuseTokenSource(nil, imp), nil
case "":
return nil, errors.New("missing 'type' field in credentials")
default:
diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go
index a1e36c0..bc3ce53 100644
--- a/google/internal/externalaccount/basecredentials.go
+++ b/google/internal/externalaccount/basecredentials.go
@@ -140,11 +140,11 @@
}
scopes := c.Scopes
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
- imp := impersonateTokenSource{
- ctx: ctx,
- url: c.ServiceAccountImpersonationURL,
- scopes: scopes,
- ts: oauth2.ReuseTokenSource(nil, ts),
+ imp := ImpersonateTokenSource{
+ Ctx: ctx,
+ URL: c.ServiceAccountImpersonationURL,
+ Scopes: scopes,
+ Ts: oauth2.ReuseTokenSource(nil, ts),
}
return oauth2.ReuseTokenSource(nil, imp), nil
}
diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go
index 64edb56..8251fc8 100644
--- a/google/internal/externalaccount/impersonate.go
+++ b/google/internal/externalaccount/impersonate.go
@@ -29,30 +29,44 @@
ExpireTime string `json:"expireTime"`
}
-type impersonateTokenSource struct {
- ctx context.Context
- ts oauth2.TokenSource
+// ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL.
+// Scopes can be defined when the access token is requested.
+type ImpersonateTokenSource struct {
+ // Ctx is the execution context of the impersonation process
+ // used to perform http call to the URL. Required
+ Ctx context.Context
+ // Ts is the source credential used to generate a token on the
+ // impersonated service account. Required.
+ Ts oauth2.TokenSource
- url string
- scopes []string
+ // URL is the endpoint to call to generate a token
+ // on behalf the service account. Required.
+ URL string
+ // Scopes that the impersonated credential should have. Required.
+ Scopes []string
+ // Delegates are the service account email addresses in a delegation chain.
+ // Each service account must be granted roles/iam.serviceAccountTokenCreator
+ // on the next service account in the chain. Optional.
+ Delegates []string
}
// Token performs the exchange to get a temporary service account token to allow access to GCP.
-func (its impersonateTokenSource) Token() (*oauth2.Token, error) {
+func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
reqBody := generateAccessTokenReq{
- Lifetime: "3600s",
- Scope: its.scopes,
+ Lifetime: "3600s",
+ Scope: its.Scopes,
+ Delegates: its.Delegates,
}
b, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err)
}
- client := oauth2.NewClient(its.ctx, its.ts)
- req, err := http.NewRequest("POST", its.url, bytes.NewReader(b))
+ client := oauth2.NewClient(its.Ctx, its.Ts)
+ req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err)
}
- req = req.WithContext(its.ctx)
+ req = req.WithContext(its.Ctx)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)