impersonate.go 3.8 KB

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