blob: b465bbcd12e3ea1f7a373eb62cc93df943080c76 [file] [log] [blame]
// 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
}