impersonate.go raw

   1  // Copyright 2021 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
   6  
   7  import (
   8  	"bytes"
   9  	"context"
  10  	"encoding/json"
  11  	"fmt"
  12  	"io"
  13  	"net/http"
  14  	"time"
  15  
  16  	"golang.org/x/oauth2"
  17  )
  18  
  19  // generateAccesstokenReq is used for service account impersonation
  20  type generateAccessTokenReq struct {
  21  	Delegates []string `json:"delegates,omitempty"`
  22  	Lifetime  string   `json:"lifetime,omitempty"`
  23  	Scope     []string `json:"scope,omitempty"`
  24  }
  25  
  26  type impersonateTokenResponse struct {
  27  	AccessToken string `json:"accessToken"`
  28  	ExpireTime  string `json:"expireTime"`
  29  }
  30  
  31  // ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL.
  32  // Scopes can be defined when the access token is requested.
  33  type ImpersonateTokenSource struct {
  34  	// Ctx is the execution context of the impersonation process
  35  	// used to perform http call to the URL. Required
  36  	Ctx context.Context
  37  	// Ts is the source credential used to generate a token on the
  38  	// impersonated service account. Required.
  39  	Ts oauth2.TokenSource
  40  
  41  	// URL is the endpoint to call to generate a token
  42  	// on behalf the service account. Required.
  43  	URL string
  44  	// Scopes that the impersonated credential should have. Required.
  45  	Scopes []string
  46  	// Delegates are the service account email addresses in a delegation chain.
  47  	// Each service account must be granted roles/iam.serviceAccountTokenCreator
  48  	// on the next service account in the chain. Optional.
  49  	Delegates []string
  50  	// TokenLifetimeSeconds is the number of seconds the impersonation token will
  51  	// be valid for.
  52  	TokenLifetimeSeconds int
  53  }
  54  
  55  // Token performs the exchange to get a temporary service account token to allow access to GCP.
  56  func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
  57  	lifetimeString := "3600s"
  58  	if its.TokenLifetimeSeconds != 0 {
  59  		lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds)
  60  	}
  61  	reqBody := generateAccessTokenReq{
  62  		Lifetime:  lifetimeString,
  63  		Scope:     its.Scopes,
  64  		Delegates: its.Delegates,
  65  	}
  66  	b, err := json.Marshal(reqBody)
  67  	if err != nil {
  68  		return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err)
  69  	}
  70  	client := oauth2.NewClient(its.Ctx, its.Ts)
  71  	req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b))
  72  	if err != nil {
  73  		return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err)
  74  	}
  75  	req = req.WithContext(its.Ctx)
  76  	req.Header.Set("Content-Type", "application/json")
  77  
  78  	resp, err := client.Do(req)
  79  	if err != nil {
  80  		return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err)
  81  	}
  82  	defer resp.Body.Close()
  83  	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
  84  	if err != nil {
  85  		return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err)
  86  	}
  87  	if c := resp.StatusCode; c < 200 || c > 299 {
  88  		return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
  89  	}
  90  
  91  	var accessTokenResp impersonateTokenResponse
  92  	if err := json.Unmarshal(body, &accessTokenResp); err != nil {
  93  		return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err)
  94  	}
  95  	expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
  96  	if err != nil {
  97  		return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err)
  98  	}
  99  	return &oauth2.Token{
 100  		AccessToken: accessTokenResp.AccessToken,
 101  		Expiry:      expiry,
 102  		TokenType:   "Bearer",
 103  	}, nil
 104  }
 105