impersonate.go raw

   1  // Copyright 2020 The Go Authors. All rights reserved.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  // Package impersonate is used to impersonate Google Credentials.
   6  package impersonate
   7  
   8  import (
   9  	"bytes"
  10  	"context"
  11  	"encoding/json"
  12  	"fmt"
  13  	"io"
  14  	"net/http"
  15  	"time"
  16  
  17  	"golang.org/x/oauth2"
  18  )
  19  
  20  // Config for generating impersonated credentials.
  21  type Config struct {
  22  	// Target is the service account to impersonate. Required.
  23  	Target string
  24  	// Scopes the impersonated credential should have. Required.
  25  	Scopes []string
  26  	// Delegates are the service accounts in a delegation chain. Each service
  27  	// account must be granted roles/iam.serviceAccountTokenCreator on the next
  28  	// service account in the chain. Optional.
  29  	Delegates []string
  30  }
  31  
  32  // TokenSource returns an impersonated TokenSource configured with the provided
  33  // config using ts as the base credential provider for making requests.
  34  func TokenSource(ctx context.Context, ts oauth2.TokenSource, config *Config) (oauth2.TokenSource, error) {
  35  	if len(config.Scopes) == 0 {
  36  		return nil, fmt.Errorf("impersonate: scopes must be provided")
  37  	}
  38  	its := impersonatedTokenSource{
  39  		ctx:  ctx,
  40  		ts:   ts,
  41  		name: formatIAMServiceAccountName(config.Target),
  42  		// Default to the longest acceptable value of one hour as the token will
  43  		// be refreshed automatically.
  44  		lifetime: "3600s",
  45  	}
  46  
  47  	its.delegates = make([]string, len(config.Delegates))
  48  	for i, v := range config.Delegates {
  49  		its.delegates[i] = formatIAMServiceAccountName(v)
  50  	}
  51  	its.scopes = make([]string, len(config.Scopes))
  52  	copy(its.scopes, config.Scopes)
  53  
  54  	return oauth2.ReuseTokenSource(nil, its), nil
  55  }
  56  
  57  func formatIAMServiceAccountName(name string) string {
  58  	return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
  59  }
  60  
  61  type generateAccessTokenReq struct {
  62  	Delegates []string `json:"delegates,omitempty"`
  63  	Lifetime  string   `json:"lifetime,omitempty"`
  64  	Scope     []string `json:"scope,omitempty"`
  65  }
  66  
  67  type generateAccessTokenResp struct {
  68  	AccessToken string `json:"accessToken"`
  69  	ExpireTime  string `json:"expireTime"`
  70  }
  71  
  72  type impersonatedTokenSource struct {
  73  	ctx context.Context
  74  	ts  oauth2.TokenSource
  75  
  76  	name      string
  77  	lifetime  string
  78  	scopes    []string
  79  	delegates []string
  80  }
  81  
  82  // Token returns an impersonated Token.
  83  func (i impersonatedTokenSource) Token() (*oauth2.Token, error) {
  84  	hc := oauth2.NewClient(i.ctx, i.ts)
  85  	reqBody := generateAccessTokenReq{
  86  		Delegates: i.delegates,
  87  		Lifetime:  i.lifetime,
  88  		Scope:     i.scopes,
  89  	}
  90  	b, err := json.Marshal(reqBody)
  91  	if err != nil {
  92  		return nil, fmt.Errorf("impersonate: unable to marshal request: %v", err)
  93  	}
  94  	url := fmt.Sprintf("https://iamcredentials.googleapis.com/v1/%s:generateAccessToken", i.name)
  95  	req, err := http.NewRequest("POST", url, bytes.NewReader(b))
  96  	if err != nil {
  97  		return nil, fmt.Errorf("impersonate: unable to create request: %v", err)
  98  	}
  99  	req = req.WithContext(i.ctx)
 100  	req.Header.Set("Content-Type", "application/json")
 101  
 102  	resp, err := hc.Do(req)
 103  	if err != nil {
 104  		return nil, fmt.Errorf("impersonate: unable to generate access token: %v", err)
 105  	}
 106  	defer resp.Body.Close()
 107  	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
 108  	if err != nil {
 109  		return nil, fmt.Errorf("impersonate: unable to read body: %v", err)
 110  	}
 111  	if c := resp.StatusCode; c < 200 || c > 299 {
 112  		return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
 113  	}
 114  
 115  	var accessTokenResp generateAccessTokenResp
 116  	if err := json.Unmarshal(body, &accessTokenResp); err != nil {
 117  		return nil, fmt.Errorf("impersonate: unable to parse response: %v", err)
 118  	}
 119  	expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
 120  	if err != nil {
 121  		return nil, fmt.Errorf("impersonate: unable to parse expiry: %v", err)
 122  	}
 123  	return &oauth2.Token{
 124  		AccessToken: accessTokenResp.AccessToken,
 125  		Expiry:      expiry,
 126  	}, nil
 127  }
 128