1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package impersonate
16
17 import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "net/http"
24 "time"
25
26 "cloud.google.com/go/auth"
27 "cloud.google.com/go/auth/credentials"
28 "cloud.google.com/go/auth/httptransport"
29 "cloud.google.com/go/auth/internal"
30 )
31
32 var (
33 iamCredentialsEndpoint = "https://iamcredentials.googleapis.com"
34 oauth2Endpoint = "https://oauth2.googleapis.com"
35 errMissingTargetPrincipal = errors.New("impersonate: target service account must be provided")
36 errMissingScopes = errors.New("impersonate: scopes must be provided")
37 errLifetimeOverMax = errors.New("impersonate: max lifetime is 12 hours")
38 errUniverseNotSupportedDomainWideDelegation = errors.New("impersonate: service account user is configured for the credential. " +
39 "Domain-wide delegation is not supported in universes other than googleapis.com")
40 )
41
42
43
44
45
46
47
48 func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) {
49 if err := opts.validate(); err != nil {
50 return nil, err
51 }
52
53 var isStaticToken bool
54
55
56 lifetime := 1 * time.Hour
57 if opts.Lifetime != 0 {
58 lifetime = opts.Lifetime
59
60 isStaticToken = true
61 }
62
63 var client *http.Client
64 var creds *auth.Credentials
65 if opts.Client == nil && opts.Credentials == nil {
66 var err error
67 creds, err = credentials.DetectDefault(&credentials.DetectOptions{
68 Scopes: []string{defaultScope},
69 UseSelfSignedJWT: true,
70 })
71 if err != nil {
72 return nil, err
73 }
74 client, err = httptransport.NewClient(&httptransport.Options{
75 Credentials: creds,
76 })
77 if err != nil {
78 return nil, err
79 }
80 } else if opts.Credentials != nil {
81 creds = opts.Credentials
82 client = internal.CloneDefaultClient()
83 if err := httptransport.AddAuthorizationMiddleware(client, opts.Credentials); err != nil {
84 return nil, err
85 }
86 } else {
87 client = opts.Client
88 }
89
90
91
92 if opts.Subject != "" {
93 if !opts.isUniverseDomainGDU() {
94 return nil, errUniverseNotSupportedDomainWideDelegation
95 }
96 tp, err := user(opts, client, lifetime, isStaticToken)
97 if err != nil {
98 return nil, err
99 }
100 var udp auth.CredentialsPropertyProvider
101 if creds != nil {
102 udp = auth.CredentialsPropertyFunc(creds.UniverseDomain)
103 }
104 return auth.NewCredentials(&auth.CredentialsOptions{
105 TokenProvider: tp,
106 UniverseDomainProvider: udp,
107 }), nil
108 }
109
110 its := impersonatedTokenProvider{
111 client: client,
112 targetPrincipal: opts.TargetPrincipal,
113 lifetime: fmt.Sprintf("%.fs", lifetime.Seconds()),
114 }
115 for _, v := range opts.Delegates {
116 its.delegates = append(its.delegates, formatIAMServiceAccountName(v))
117 }
118 its.scopes = make([]string, len(opts.Scopes))
119 copy(its.scopes, opts.Scopes)
120
121 var tpo *auth.CachedTokenProviderOptions
122 if isStaticToken {
123 tpo = &auth.CachedTokenProviderOptions{
124 DisableAutoRefresh: true,
125 }
126 }
127
128 var udp auth.CredentialsPropertyProvider
129 if creds != nil {
130 udp = auth.CredentialsPropertyFunc(creds.UniverseDomain)
131 }
132 return auth.NewCredentials(&auth.CredentialsOptions{
133 TokenProvider: auth.NewCachedTokenProvider(its, tpo),
134 UniverseDomainProvider: udp,
135 }), nil
136 }
137
138
139 type CredentialsOptions struct {
140
141
142 TargetPrincipal string
143
144 Scopes []string
145
146
147
148 Delegates []string
149
150
151
152
153
154
155 Lifetime time.Duration
156
157
158
159 Subject string
160
161
162
163
164 Credentials *auth.Credentials
165
166
167
168 Client *http.Client
169
170
171 UniverseDomain string
172 }
173
174 func (o *CredentialsOptions) validate() error {
175 if o == nil {
176 return errors.New("impersonate: options must be provided")
177 }
178 if o.TargetPrincipal == "" {
179 return errMissingTargetPrincipal
180 }
181 if len(o.Scopes) == 0 {
182 return errMissingScopes
183 }
184 if o.Lifetime.Hours() > 12 {
185 return errLifetimeOverMax
186 }
187 return nil
188 }
189
190
191
192 func (o *CredentialsOptions) getUniverseDomain() string {
193 if o.UniverseDomain == "" {
194 return internal.DefaultUniverseDomain
195 }
196 return o.UniverseDomain
197 }
198
199
200
201 func (o *CredentialsOptions) isUniverseDomainGDU() bool {
202 return o.getUniverseDomain() == internal.DefaultUniverseDomain
203 }
204
205 func formatIAMServiceAccountName(name string) string {
206 return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
207 }
208
209 type generateAccessTokenRequest struct {
210 Delegates []string `json:"delegates,omitempty"`
211 Lifetime string `json:"lifetime,omitempty"`
212 Scope []string `json:"scope,omitempty"`
213 }
214
215 type generateAccessTokenResponse struct {
216 AccessToken string `json:"accessToken"`
217 ExpireTime string `json:"expireTime"`
218 }
219
220 type impersonatedTokenProvider struct {
221 client *http.Client
222
223 targetPrincipal string
224 lifetime string
225 scopes []string
226 delegates []string
227 }
228
229
230 func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
231 reqBody := generateAccessTokenRequest{
232 Delegates: i.delegates,
233 Lifetime: i.lifetime,
234 Scope: i.scopes,
235 }
236 b, err := json.Marshal(reqBody)
237 if err != nil {
238 return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
239 }
240 url := fmt.Sprintf("%s/v1/%s:generateAccessToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal))
241 req, err := http.NewRequest("POST", url, bytes.NewReader(b))
242 if err != nil {
243 return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
244 }
245 req.Header.Set("Content-Type", "application/json")
246
247 resp, err := i.client.Do(req)
248 if err != nil {
249 return nil, fmt.Errorf("impersonate: unable to generate access token: %w", err)
250 }
251 defer resp.Body.Close()
252 body, err := internal.ReadAll(resp.Body)
253 if err != nil {
254 return nil, fmt.Errorf("impersonate: unable to read body: %w", err)
255 }
256 if c := resp.StatusCode; c < 200 || c > 299 {
257 return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
258 }
259
260 var accessTokenResp generateAccessTokenResponse
261 if err := json.Unmarshal(body, &accessTokenResp); err != nil {
262 return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
263 }
264 expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
265 if err != nil {
266 return nil, fmt.Errorf("impersonate: unable to parse expiry: %w", err)
267 }
268 return &auth.Token{
269 Value: accessTokenResp.AccessToken,
270 Expiry: expiry,
271 }, nil
272 }
273
View as plain text