1
2
3
4
5
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
21 type Config struct {
22
23 Target string
24
25 Scopes []string
26
27
28
29 Delegates []string
30 }
31
32
33
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
43
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
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
View as plain text