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: 14806e6b37
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>
This commit is contained in:
Guillaume Blaquiere 2021-10-28 17:47:33 +00:00 committed by Cody Oss
parent 6b3c2da341
commit ba495a64dc
3 changed files with 55 additions and 19 deletions

View File

@ -95,6 +95,7 @@ const (
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 @@ type credentialsFile struct {
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 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
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:

View File

@ -140,11 +140,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
}
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
}

View File

@ -29,30 +29,44 @@ type impersonateTokenResponse struct {
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,
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)