...

Source file src/google.golang.org/api/impersonate/impersonate.go

Documentation: google.golang.org/api/impersonate

     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  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"net/http"
    15  	"time"
    16  
    17  	"golang.org/x/oauth2"
    18  	"google.golang.org/api/internal"
    19  	"google.golang.org/api/option"
    20  	"google.golang.org/api/option/internaloption"
    21  	htransport "google.golang.org/api/transport/http"
    22  )
    23  
    24  var (
    25  	iamCredentailsEndpoint                      = "https://iamcredentials.googleapis.com"
    26  	oauth2Endpoint                              = "https://oauth2.googleapis.com"
    27  	errMissingTargetPrincipal                   = errors.New("impersonate: a target service account must be provided")
    28  	errMissingScopes                            = errors.New("impersonate: scopes must be provided")
    29  	errLifetimeOverMax                          = errors.New("impersonate: max lifetime is 12 hours")
    30  	errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " +
    31  		"Domain-wide delegation is not supported in universes other than googleapis.com")
    32  )
    33  
    34  // CredentialsConfig for generating impersonated credentials.
    35  type CredentialsConfig struct {
    36  	// TargetPrincipal is the email address of the service account to
    37  	// impersonate. Required.
    38  	TargetPrincipal string
    39  	// Scopes that the impersonated credential should have. Required.
    40  	Scopes []string
    41  	// Delegates are the service account email addresses in a delegation chain.
    42  	// Each service account must be granted roles/iam.serviceAccountTokenCreator
    43  	// on the next service account in the chain. Optional.
    44  	Delegates []string
    45  	// Lifetime is the amount of time until the impersonated token expires. If
    46  	// unset the token's lifetime will be one hour and be automatically
    47  	// refreshed. If set the token may have a max lifetime of one hour and will
    48  	// not be refreshed. Service accounts that have been added to an org policy
    49  	// with constraints/iam.allowServiceAccountCredentialLifetimeExtension may
    50  	// request a token lifetime of up to 12 hours. Optional.
    51  	Lifetime time.Duration
    52  	// Subject is the sub field of a JWT. This field should only be set if you
    53  	// wish to impersonate as a user. This feature is useful when using domain
    54  	// wide delegation. Optional.
    55  	Subject string
    56  }
    57  
    58  // defaultClientOptions ensures the base credentials will work with the IAM
    59  // Credentials API if no scope or audience is set by the user.
    60  func defaultClientOptions() []option.ClientOption {
    61  	return []option.ClientOption{
    62  		internaloption.WithDefaultAudience("https://iamcredentials.googleapis.com/"),
    63  		internaloption.WithDefaultScopes("https://www.googleapis.com/auth/cloud-platform"),
    64  	}
    65  }
    66  
    67  // CredentialsTokenSource returns an impersonated CredentialsTokenSource configured with the provided
    68  // config and using credentials loaded from Application Default Credentials as
    69  // the base credentials.
    70  func CredentialsTokenSource(ctx context.Context, config CredentialsConfig, opts ...option.ClientOption) (oauth2.TokenSource, error) {
    71  	if config.TargetPrincipal == "" {
    72  		return nil, errMissingTargetPrincipal
    73  	}
    74  	if len(config.Scopes) == 0 {
    75  		return nil, errMissingScopes
    76  	}
    77  	if config.Lifetime.Hours() > 12 {
    78  		return nil, errLifetimeOverMax
    79  	}
    80  
    81  	var isStaticToken bool
    82  	// Default to the longest acceptable value of one hour as the token will
    83  	// be refreshed automatically if not set.
    84  	lifetime := 3600 * time.Second
    85  	if config.Lifetime != 0 {
    86  		lifetime = config.Lifetime
    87  		// Don't auto-refresh token if a lifetime is configured.
    88  		isStaticToken = true
    89  	}
    90  
    91  	clientOpts := append(defaultClientOptions(), opts...)
    92  	client, _, err := htransport.NewClient(ctx, clientOpts...)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	// If a subject is specified a domain-wide delegation auth-flow is initiated
    97  	// to impersonate as the provided subject (user).
    98  	if config.Subject != "" {
    99  		settings, err := newSettings(clientOpts)
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  		if !settings.IsUniverseDomainGDU() {
   104  			return nil, errUniverseNotSupportedDomainWideDelegation
   105  		}
   106  		return user(ctx, config, client, lifetime, isStaticToken)
   107  	}
   108  
   109  	its := impersonatedTokenSource{
   110  		client:          client,
   111  		targetPrincipal: config.TargetPrincipal,
   112  		lifetime:        fmt.Sprintf("%.fs", lifetime.Seconds()),
   113  	}
   114  	for _, v := range config.Delegates {
   115  		its.delegates = append(its.delegates, formatIAMServiceAccountName(v))
   116  	}
   117  	its.scopes = make([]string, len(config.Scopes))
   118  	copy(its.scopes, config.Scopes)
   119  
   120  	if isStaticToken {
   121  		tok, err := its.Token()
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		return oauth2.StaticTokenSource(tok), nil
   126  	}
   127  	return oauth2.ReuseTokenSource(nil, its), nil
   128  }
   129  
   130  func newSettings(opts []option.ClientOption) (*internal.DialSettings, error) {
   131  	var o internal.DialSettings
   132  	for _, opt := range opts {
   133  		opt.Apply(&o)
   134  	}
   135  	if err := o.Validate(); err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	return &o, nil
   140  }
   141  
   142  func formatIAMServiceAccountName(name string) string {
   143  	return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
   144  }
   145  
   146  type generateAccessTokenReq struct {
   147  	Delegates []string `json:"delegates,omitempty"`
   148  	Lifetime  string   `json:"lifetime,omitempty"`
   149  	Scope     []string `json:"scope,omitempty"`
   150  }
   151  
   152  type generateAccessTokenResp struct {
   153  	AccessToken string `json:"accessToken"`
   154  	ExpireTime  string `json:"expireTime"`
   155  }
   156  
   157  type impersonatedTokenSource struct {
   158  	client *http.Client
   159  
   160  	targetPrincipal string
   161  	lifetime        string
   162  	scopes          []string
   163  	delegates       []string
   164  }
   165  
   166  // Token returns an impersonated Token.
   167  func (i impersonatedTokenSource) Token() (*oauth2.Token, error) {
   168  	reqBody := generateAccessTokenReq{
   169  		Delegates: i.delegates,
   170  		Lifetime:  i.lifetime,
   171  		Scope:     i.scopes,
   172  	}
   173  	b, err := json.Marshal(reqBody)
   174  	if err != nil {
   175  		return nil, fmt.Errorf("impersonate: unable to marshal request: %v", err)
   176  	}
   177  	url := fmt.Sprintf("%s/v1/%s:generateAccessToken", iamCredentailsEndpoint, formatIAMServiceAccountName(i.targetPrincipal))
   178  	req, err := http.NewRequest("POST", url, bytes.NewReader(b))
   179  	if err != nil {
   180  		return nil, fmt.Errorf("impersonate: unable to create request: %v", err)
   181  	}
   182  	req.Header.Set("Content-Type", "application/json")
   183  
   184  	resp, err := i.client.Do(req)
   185  	if err != nil {
   186  		return nil, fmt.Errorf("impersonate: unable to generate access token: %v", err)
   187  	}
   188  	defer resp.Body.Close()
   189  	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
   190  	if err != nil {
   191  		return nil, fmt.Errorf("impersonate: unable to read body: %v", err)
   192  	}
   193  	if c := resp.StatusCode; c < 200 || c > 299 {
   194  		return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
   195  	}
   196  
   197  	var accessTokenResp generateAccessTokenResp
   198  	if err := json.Unmarshal(body, &accessTokenResp); err != nil {
   199  		return nil, fmt.Errorf("impersonate: unable to parse response: %v", err)
   200  	}
   201  	expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
   202  	if err != nil {
   203  		return nil, fmt.Errorf("impersonate: unable to parse expiry: %v", err)
   204  	}
   205  	return &oauth2.Token{
   206  		AccessToken: accessTokenResp.AccessToken,
   207  		Expiry:      expiry,
   208  	}, nil
   209  }
   210  

View as plain text