| // 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 |
| } |