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