1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package externalaccount
16
17 import (
18 "context"
19 "errors"
20 "fmt"
21 "net/http"
22 "regexp"
23 "strconv"
24 "strings"
25 "time"
26
27 "cloud.google.com/go/auth"
28 "cloud.google.com/go/auth/credentials/internal/impersonate"
29 "cloud.google.com/go/auth/credentials/internal/stsexchange"
30 "cloud.google.com/go/auth/internal/credsfile"
31 )
32
33 const (
34 timeoutMinimum = 5 * time.Second
35 timeoutMaximum = 120 * time.Second
36
37 universeDomainPlaceholder = "UNIVERSE_DOMAIN"
38 defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
39 defaultUniverseDomain = "googleapis.com"
40 )
41
42 var (
43
44 Now = func() time.Time {
45 return time.Now().UTC()
46 }
47 validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
48 )
49
50
51 type Options struct {
52
53
54 Audience string
55
56
57 SubjectTokenType string
58
59 TokenURL string
60
61
62
63 TokenInfoURL string
64
65
66 ServiceAccountImpersonationURL string
67
68
69 ServiceAccountImpersonationLifetimeSeconds int
70
71
72
73 ClientSecret string
74
75 ClientID string
76
77
78 CredentialSource *credsfile.CredentialSource
79
80
81 QuotaProjectID string
82
83 Scopes []string
84
85
86
87
88 WorkforcePoolUserProject string
89
90
91
92 UniverseDomain string
93
94
95
96 SubjectTokenProvider SubjectTokenProvider
97
98
99
100 AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
101
102 Client *http.Client
103 }
104
105
106
107 type SubjectTokenProvider interface {
108
109
110
111
112 SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
113 }
114
115
116
117 type RequestOptions struct {
118
119 Audience string
120
121
122
123
124
125
126 SubjectTokenType string
127 }
128
129
130
131 type AwsSecurityCredentialsProvider interface {
132
133 AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
134
135
136
137
138
139 AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
140 }
141
142
143 type AwsSecurityCredentials struct {
144
145 AccessKeyID string `json:"AccessKeyID"`
146
147 SecretAccessKey string `json:"SecretAccessKey"`
148
149
150 SessionToken string `json:"Token"`
151 }
152
153 func (o *Options) validate() error {
154 if o.Audience == "" {
155 return fmt.Errorf("externalaccount: Audience must be set")
156 }
157 if o.SubjectTokenType == "" {
158 return fmt.Errorf("externalaccount: Subject token type must be set")
159 }
160 if o.WorkforcePoolUserProject != "" {
161 if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
162 return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
163 }
164 }
165 count := 0
166 if o.CredentialSource != nil {
167 count++
168 }
169 if o.SubjectTokenProvider != nil {
170 count++
171 }
172 if o.AwsSecurityCredentialsProvider != nil {
173 count++
174 }
175 if count == 0 {
176 return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
177 }
178 if count > 1 {
179 return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
180 }
181 return nil
182 }
183
184
185
186 func (o *Options) resolveTokenURL() {
187 if o.TokenURL != "" {
188 return
189 } else if o.UniverseDomain != "" {
190 o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
191 } else {
192 o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
193 }
194 }
195
196
197
198 func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
199 if err := opts.validate(); err != nil {
200 return nil, err
201 }
202 opts.resolveTokenURL()
203 stp, err := newSubjectTokenProvider(opts)
204 if err != nil {
205 return nil, err
206 }
207 tp := &tokenProvider{
208 client: opts.Client,
209 opts: opts,
210 stp: stp,
211 }
212 if opts.ServiceAccountImpersonationURL == "" {
213 return auth.NewCachedTokenProvider(tp, nil), nil
214 }
215
216 scopes := make([]string, len(opts.Scopes))
217 copy(scopes, opts.Scopes)
218
219 tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
220 imp, err := impersonate.NewTokenProvider(&impersonate.Options{
221 Client: opts.Client,
222 URL: opts.ServiceAccountImpersonationURL,
223 Scopes: scopes,
224 Tp: auth.NewCachedTokenProvider(tp, nil),
225 TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
226 })
227 if err != nil {
228 return nil, err
229 }
230 return auth.NewCachedTokenProvider(imp, nil), nil
231 }
232
233 type subjectTokenProvider interface {
234 subjectToken(ctx context.Context) (string, error)
235 providerType() string
236 }
237
238
239 type tokenProvider struct {
240 client *http.Client
241 opts *Options
242 stp subjectTokenProvider
243 }
244
245 func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
246 subjectToken, err := tp.stp.subjectToken(ctx)
247 if err != nil {
248 return nil, err
249 }
250
251 stsRequest := &stsexchange.TokenRequest{
252 GrantType: stsexchange.GrantType,
253 Audience: tp.opts.Audience,
254 Scope: tp.opts.Scopes,
255 RequestedTokenType: stsexchange.TokenType,
256 SubjectToken: subjectToken,
257 SubjectTokenType: tp.opts.SubjectTokenType,
258 }
259 header := make(http.Header)
260 header.Set("Content-Type", "application/x-www-form-urlencoded")
261 header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
262 clientAuth := stsexchange.ClientAuthentication{
263 AuthStyle: auth.StyleInHeader,
264 ClientID: tp.opts.ClientID,
265 ClientSecret: tp.opts.ClientSecret,
266 }
267 var options map[string]interface{}
268
269
270 if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
271 options = map[string]interface{}{
272 "userProject": tp.opts.WorkforcePoolUserProject,
273 }
274 }
275 stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
276 Client: tp.client,
277 Endpoint: tp.opts.TokenURL,
278 Request: stsRequest,
279 Authentication: clientAuth,
280 Headers: header,
281 ExtraOpts: options,
282 })
283 if err != nil {
284 return nil, err
285 }
286
287 tok := &auth.Token{
288 Value: stsResp.AccessToken,
289 Type: stsResp.TokenType,
290 }
291
292 if stsResp.ExpiresIn <= 0 {
293 return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
294 }
295 tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
296 return tok, nil
297 }
298
299
300
301 func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
302 reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
303 if o.AwsSecurityCredentialsProvider != nil {
304 return &awsSubjectProvider{
305 securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
306 TargetResource: o.Audience,
307 reqOpts: reqOpts,
308 }, nil
309 } else if o.SubjectTokenProvider != nil {
310 return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
311 } else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
312 if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
313 if awsVersion != 1 {
314 return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
315 }
316
317 awsProvider := &awsSubjectProvider{
318 EnvironmentID: o.CredentialSource.EnvironmentID,
319 RegionURL: o.CredentialSource.RegionURL,
320 RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
321 CredVerificationURL: o.CredentialSource.URL,
322 TargetResource: o.Audience,
323 Client: o.Client,
324 }
325 if o.CredentialSource.IMDSv2SessionTokenURL != "" {
326 awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
327 }
328
329 return awsProvider, nil
330 }
331 } else if o.CredentialSource.File != "" {
332 return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
333 } else if o.CredentialSource.URL != "" {
334 return &urlSubjectProvider{URL: o.CredentialSource.URL, Headers: o.CredentialSource.Headers, Format: o.CredentialSource.Format, Client: o.Client}, nil
335 } else if o.CredentialSource.Executable != nil {
336 ec := o.CredentialSource.Executable
337 if ec.Command == "" {
338 return nil, errors.New("credentials: missing `command` field — executable command must be provided")
339 }
340
341 execProvider := &executableSubjectProvider{}
342 execProvider.Command = ec.Command
343 if ec.TimeoutMillis == 0 {
344 execProvider.Timeout = executableDefaultTimeout
345 } else {
346 execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
347 if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
348 return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
349 }
350 }
351 execProvider.OutputFile = ec.OutputFile
352 execProvider.client = o.Client
353 execProvider.opts = o
354 execProvider.env = runtimeEnvironment{}
355 return execProvider, nil
356 }
357 return nil, errors.New("credentials: unable to parse credential source")
358 }
359
360 func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
361 return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
362 goVersion(),
363 "unknown",
364 p.providerType(),
365 conf.ServiceAccountImpersonationURL != "",
366 conf.ServiceAccountImpersonationLifetimeSeconds != 0)
367 }
368
View as plain text