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

@ -92,9 +92,10 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
// JSON key file types. // JSON key file types.
const ( const (
serviceAccountKey = "service_account" serviceAccountKey = "service_account"
userCredentialsKey = "authorized_user" userCredentialsKey = "authorized_user"
externalAccountKey = "external_account" externalAccountKey = "external_account"
impersonatedServiceAccount = "impersonated_service_account"
) )
// credentialsFile is the unmarshalled representation of a credentials file. // credentialsFile is the unmarshalled representation of a credentials file.
@ -121,9 +122,13 @@ type credentialsFile struct {
TokenURLExternal string `json:"token_url"` TokenURLExternal string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"` TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
CredentialSource externalaccount.CredentialSource `json:"credential_source"` CredentialSource externalaccount.CredentialSource `json:"credential_source"`
QuotaProjectID string `json:"quota_project_id"` QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"` 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 { 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, WorkforcePoolUserProject: f.WorkforcePoolUserProject,
} }
return cfg.TokenSource(ctx) 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 "": case "":
return nil, errors.New("missing 'type' field in credentials") return nil, errors.New("missing 'type' field in credentials")
default: default:

View File

@ -140,11 +140,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
} }
scopes := c.Scopes scopes := c.Scopes
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
imp := impersonateTokenSource{ imp := ImpersonateTokenSource{
ctx: ctx, Ctx: ctx,
url: c.ServiceAccountImpersonationURL, URL: c.ServiceAccountImpersonationURL,
scopes: scopes, Scopes: scopes,
ts: oauth2.ReuseTokenSource(nil, ts), Ts: oauth2.ReuseTokenSource(nil, ts),
} }
return oauth2.ReuseTokenSource(nil, imp), nil return oauth2.ReuseTokenSource(nil, imp), nil
} }

View File

@ -29,30 +29,44 @@ type impersonateTokenResponse struct {
ExpireTime string `json:"expireTime"` ExpireTime string `json:"expireTime"`
} }
type impersonateTokenSource struct { // ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL.
ctx context.Context // Scopes can be defined when the access token is requested.
ts oauth2.TokenSource 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 // URL is the endpoint to call to generate a token
scopes []string // 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. // 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{ reqBody := generateAccessTokenReq{
Lifetime: "3600s", Lifetime: "3600s",
Scope: its.scopes, Scope: its.Scopes,
Delegates: its.Delegates,
} }
b, err := json.Marshal(reqBody) b, err := json.Marshal(reqBody)
if err != nil { if err != nil {
return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err) return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err)
} }
client := oauth2.NewClient(its.ctx, its.ts) client := oauth2.NewClient(its.Ctx, its.Ts)
req, err := http.NewRequest("POST", its.url, bytes.NewReader(b)) req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b))
if err != nil { if err != nil {
return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) 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") req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req) resp, err := client.Do(req)