github.com/argoproj/argo-cd/v3@v3.2.1/util/git/creds.go (about) 1 package git 2 3 import ( 4 "context" 5 "crypto/sha256" 6 "encoding/base64" 7 "encoding/hex" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/google/go-github/v69/github" 20 21 "golang.org/x/oauth2" 22 "golang.org/x/oauth2/google" 23 24 gocache "github.com/patrickmn/go-cache" 25 26 argoio "github.com/argoproj/gitops-engine/pkg/utils/io" 27 "github.com/argoproj/gitops-engine/pkg/utils/text" 28 "github.com/bradleyfalzon/ghinstallation/v2" 29 log "github.com/sirupsen/logrus" 30 31 "github.com/argoproj/argo-cd/v3/common" 32 argoutils "github.com/argoproj/argo-cd/v3/util" 33 certutil "github.com/argoproj/argo-cd/v3/util/cert" 34 utilio "github.com/argoproj/argo-cd/v3/util/io" 35 "github.com/argoproj/argo-cd/v3/util/workloadidentity" 36 ) 37 38 var ( 39 // In memory cache for storing github APP api token credentials 40 githubAppTokenCache *gocache.Cache 41 // In memory cache for storing oauth2.TokenSource used to generate Google Cloud OAuth tokens 42 googleCloudTokenSource *gocache.Cache 43 44 // In memory cache for storing Azure tokens 45 azureTokenCache *gocache.Cache 46 ) 47 48 const ( 49 // githubAccessTokenUsername is a username that is used to with the github access token 50 githubAccessTokenUsername = "x-access-token" 51 forceBasicAuthHeaderEnv = "ARGOCD_GIT_AUTH_HEADER" 52 bearerAuthHeaderEnv = "ARGOCD_GIT_BEARER_AUTH_HEADER" 53 // This is the resource id of the OAuth application of Azure Devops. 54 azureDevopsEntraResourceId = "499b84ac-1321-427f-aa17-267ca6975798/.default" 55 ) 56 57 func init() { 58 githubAppCredsExp := common.GithubAppCredsExpirationDuration 59 if exp := os.Getenv(common.EnvGithubAppCredsExpirationDuration); exp != "" { 60 if qps, err := strconv.Atoi(exp); err != nil { 61 githubAppCredsExp = time.Duration(qps) * time.Minute 62 } 63 } 64 65 githubAppTokenCache = gocache.New(githubAppCredsExp, 1*time.Minute) 66 // oauth2.TokenSource handles fetching new Tokens once they are expired. The oauth2.TokenSource itself does not expire. 67 googleCloudTokenSource = gocache.New(gocache.NoExpiration, 0) 68 azureTokenCache = gocache.New(gocache.NoExpiration, 0) 69 } 70 71 type NoopCredsStore struct{} 72 73 func (d NoopCredsStore) Add(_ string, _ string) string { 74 return "" 75 } 76 77 func (d NoopCredsStore) Remove(_ string) { 78 } 79 80 func (d NoopCredsStore) Environ(_ string) []string { 81 return []string{} 82 } 83 84 type CredsStore interface { 85 Add(username string, password string) string 86 Remove(id string) 87 // Environ returns the environment variables that should be set to use the credentials for the given credential ID. 88 Environ(id string) []string 89 } 90 91 type Creds interface { 92 Environ() (io.Closer, []string, error) 93 // GetUserInfo gets the username and email address for the credentials, if they're available. 94 GetUserInfo(ctx context.Context) (string, string, error) 95 } 96 97 // nop implementation 98 type NopCloser struct{} 99 100 func (c NopCloser) Close() error { 101 return nil 102 } 103 104 var _ Creds = NopCreds{} 105 106 type NopCreds struct{} 107 108 func (c NopCreds) Environ() (io.Closer, []string, error) { 109 return NopCloser{}, nil, nil 110 } 111 112 // GetUserInfo returns empty strings for user info 113 func (c NopCreds) GetUserInfo(_ context.Context) (name string, email string, err error) { 114 return "", "", nil 115 } 116 117 var _ io.Closer = NopCloser{} 118 119 type GenericHTTPSCreds interface { 120 HasClientCert() bool 121 GetClientCertData() string 122 GetClientCertKey() string 123 Creds 124 } 125 126 var ( 127 _ GenericHTTPSCreds = HTTPSCreds{} 128 _ Creds = HTTPSCreds{} 129 ) 130 131 // HTTPS creds implementation 132 type HTTPSCreds struct { 133 // Username for authentication 134 username string 135 // Password for authentication 136 password string 137 // Bearer token for authentication 138 bearerToken string 139 // Whether to ignore invalid server certificates 140 insecure bool 141 // Client certificate to use 142 clientCertData string 143 // Client certificate key to use 144 clientCertKey string 145 // temporal credentials store 146 store CredsStore 147 // whether to force usage of basic auth 148 forceBasicAuth bool 149 } 150 151 func NewHTTPSCreds(username string, password string, bearerToken string, clientCertData string, clientCertKey string, insecure bool, store CredsStore, forceBasicAuth bool) GenericHTTPSCreds { 152 return HTTPSCreds{ 153 username, 154 password, 155 bearerToken, 156 insecure, 157 clientCertData, 158 clientCertKey, 159 store, 160 forceBasicAuth, 161 } 162 } 163 164 // GetUserInfo returns the username and email address for the credentials, if they're available. 165 func (creds HTTPSCreds) GetUserInfo(_ context.Context) (string, string, error) { 166 // Email not implemented for HTTPS creds. 167 return creds.username, "", nil 168 } 169 170 func (creds HTTPSCreds) BasicAuthHeader() string { 171 h := "Authorization: Basic " 172 t := creds.username + ":" + creds.password 173 h += base64.StdEncoding.EncodeToString([]byte(t)) 174 return h 175 } 176 177 func (creds HTTPSCreds) BearerAuthHeader() string { 178 h := "Authorization: Bearer " + creds.bearerToken 179 return h 180 } 181 182 // Get additional required environment variables for executing git client to 183 // access specific repository via HTTPS. 184 func (creds HTTPSCreds) Environ() (io.Closer, []string, error) { 185 var env []string 186 187 httpCloser := authFilePaths(make([]string, 0)) 188 189 // GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at 190 // all. 191 if creds.insecure { 192 env = append(env, "GIT_SSL_NO_VERIFY=true") 193 } 194 195 // In case the repo is configured for using a TLS client cert, we need to make 196 // sure git client will use it. The certificate's key must not be password 197 // protected. 198 if creds.HasClientCert() { 199 var certFile, keyFile *os.File 200 201 // We need to actually create two temp files, one for storing cert data and 202 // another for storing the key. If we fail to create second fail, the first 203 // must be removed. 204 certFile, err := os.CreateTemp(argoio.TempDir, "") 205 if err != nil { 206 return NopCloser{}, nil, err 207 } 208 defer certFile.Close() 209 keyFile, err = os.CreateTemp(argoio.TempDir, "") 210 if err != nil { 211 removeErr := os.Remove(certFile.Name()) 212 if removeErr != nil { 213 log.Errorf("Could not remove previously created tempfile %s: %v", certFile.Name(), removeErr) 214 } 215 return NopCloser{}, nil, err 216 } 217 defer keyFile.Close() 218 219 // We should have both temp files by now 220 httpCloser = authFilePaths([]string{certFile.Name(), keyFile.Name()}) 221 222 _, err = certFile.WriteString(creds.clientCertData) 223 if err != nil { 224 httpCloser.Close() 225 return NopCloser{}, nil, err 226 } 227 // GIT_SSL_CERT is the full path to a client certificate to be used 228 env = append(env, "GIT_SSL_CERT="+certFile.Name()) 229 230 _, err = keyFile.WriteString(creds.clientCertKey) 231 if err != nil { 232 httpCloser.Close() 233 return NopCloser{}, nil, err 234 } 235 // GIT_SSL_KEY is the full path to a client certificate's key to be used 236 env = append(env, "GIT_SSL_KEY="+keyFile.Name()) 237 } 238 // If at least password is set, we will set ARGOCD_BASIC_AUTH_HEADER to 239 // hold the HTTP authorization header, so auth mechanism negotiation is 240 // skipped. This is insecure, but some environments may need it. 241 if creds.password != "" && creds.forceBasicAuth { 242 env = append(env, fmt.Sprintf("%s=%s", forceBasicAuthHeaderEnv, creds.BasicAuthHeader())) 243 } else if creds.bearerToken != "" { 244 // If bearer token is set, we will set ARGOCD_BEARER_AUTH_HEADER to hold the HTTP authorization header 245 env = append(env, fmt.Sprintf("%s=%s", bearerAuthHeaderEnv, creds.BearerAuthHeader())) 246 } 247 nonce := creds.store.Add(text.FirstNonEmpty(creds.username, githubAccessTokenUsername), creds.password) 248 env = append(env, creds.store.Environ(nonce)...) 249 return utilio.NewCloser(func() error { 250 creds.store.Remove(nonce) 251 return httpCloser.Close() 252 }), env, nil 253 } 254 255 func (creds HTTPSCreds) HasClientCert() bool { 256 return creds.clientCertData != "" && creds.clientCertKey != "" 257 } 258 259 func (creds HTTPSCreds) GetClientCertData() string { 260 return creds.clientCertData 261 } 262 263 func (creds HTTPSCreds) GetClientCertKey() string { 264 return creds.clientCertKey 265 } 266 267 var _ Creds = SSHCreds{} 268 269 // SSH implementation 270 type SSHCreds struct { 271 sshPrivateKey string 272 caPath string 273 insecure bool 274 proxy string 275 } 276 277 func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool, proxy string) SSHCreds { 278 return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey, proxy} 279 } 280 281 // GetUserInfo returns empty strings for user info. 282 // TODO: Implement this method to return the username and email address for the credentials, if they're available. 283 func (c SSHCreds) GetUserInfo(_ context.Context) (string, string, error) { 284 // User info not implemented for SSH creds. 285 return "", "", nil 286 } 287 288 type sshPrivateKeyFile string 289 290 type authFilePaths []string 291 292 func (f sshPrivateKeyFile) Close() error { 293 return os.Remove(string(f)) 294 } 295 296 // Remove a list of files that have been created as temp files while creating 297 // HTTPCreds object above. 298 func (f authFilePaths) Close() error { 299 var retErr error 300 for _, path := range f { 301 err := os.Remove(path) 302 if err != nil { 303 log.Errorf("HTTPSCreds.Close(): Could not remove temp file %s: %v", path, err) 304 retErr = err 305 } 306 } 307 return retErr 308 } 309 310 func (c SSHCreds) Environ() (io.Closer, []string, error) { 311 // use the SHM temp dir from util, more secure 312 file, err := os.CreateTemp(argoio.TempDir, "") 313 if err != nil { 314 return nil, nil, err 315 } 316 317 sshCloser := sshPrivateKeyFile(file.Name()) 318 319 defer func() { 320 if err = file.Close(); err != nil { 321 log.WithFields(log.Fields{ 322 common.SecurityField: common.SecurityMedium, 323 common.SecurityCWEField: common.SecurityCWEMissingReleaseOfFileDescriptor, 324 }).Errorf("error closing file %q: %v", file.Name(), err) 325 } 326 }() 327 328 _, err = file.WriteString(c.sshPrivateKey + "\n") 329 if err != nil { 330 sshCloser.Close() 331 return nil, nil, err 332 } 333 334 args := []string{"ssh", "-i", file.Name()} 335 var env []string 336 if c.caPath != "" { 337 env = append(env, "GIT_SSL_CAINFO="+c.caPath) 338 } 339 if c.insecure { 340 log.Warn("temporarily disabling strict host key checking (i.e. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'), please don't use in production") 341 // StrictHostKeyChecking will add the host to the knownhosts file, we don't want that - a security issue really, 342 // UserKnownHostsFile=/dev/null is therefore used so we write the new insecure host to /dev/null 343 args = append(args, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null") 344 } else { 345 knownHostsFile := certutil.GetSSHKnownHostsDataPath() 346 args = append(args, "-o", "StrictHostKeyChecking=yes", "-o", "UserKnownHostsFile="+knownHostsFile) 347 } 348 // Handle SSH socks5 proxy settings 349 proxyEnv := []string{} 350 if c.proxy != "" { 351 parsedProxyURL, err := url.Parse(c.proxy) 352 if err != nil { 353 sshCloser.Close() 354 return nil, nil, fmt.Errorf("failed to set environment variables related to socks5 proxy, could not parse proxy URL '%s': %w", c.proxy, err) 355 } 356 args = append(args, "-o", fmt.Sprintf("ProxyCommand='connect-proxy -S %s:%s -5 %%h %%p'", 357 parsedProxyURL.Hostname(), 358 parsedProxyURL.Port())) 359 if parsedProxyURL.User != nil { 360 proxyEnv = append(proxyEnv, "SOCKS5_USER="+parsedProxyURL.User.Username()) 361 if socks5Passwd, isPasswdSet := parsedProxyURL.User.Password(); isPasswdSet { 362 proxyEnv = append(proxyEnv, "SOCKS5_PASSWD="+socks5Passwd) 363 } 364 } 365 } 366 env = append(env, []string{"GIT_SSH_COMMAND=" + strings.Join(args, " ")}...) 367 env = append(env, proxyEnv...) 368 return sshCloser, env, nil 369 } 370 371 // GitHubAppCreds to authenticate as GitHub application 372 type GitHubAppCreds struct { 373 appID int64 374 appInstallId int64 375 privateKey string 376 baseURL string 377 clientCertData string 378 clientCertKey string 379 insecure bool 380 proxy string 381 noProxy string 382 store CredsStore 383 } 384 385 // NewGitHubAppCreds provide github app credentials 386 func NewGitHubAppCreds(appID int64, appInstallId int64, privateKey string, baseURL string, clientCertData string, clientCertKey string, insecure bool, proxy string, noProxy string, store CredsStore) GenericHTTPSCreds { 387 return GitHubAppCreds{appID: appID, appInstallId: appInstallId, privateKey: privateKey, baseURL: baseURL, clientCertData: clientCertData, clientCertKey: clientCertKey, insecure: insecure, proxy: proxy, noProxy: noProxy, store: store} 388 } 389 390 func (g GitHubAppCreds) Environ() (io.Closer, []string, error) { 391 token, err := g.getAccessToken() 392 if err != nil { 393 return NopCloser{}, nil, err 394 } 395 var env []string 396 httpCloser := authFilePaths(make([]string, 0)) 397 398 // GIT_SSL_NO_VERIFY is used to tell git not to validate the server's cert at 399 // all. 400 if g.insecure { 401 env = append(env, "GIT_SSL_NO_VERIFY=true") 402 } 403 404 // In case the repo is configured for using a TLS client cert, we need to make 405 // sure git client will use it. The certificate's key must not be password 406 // protected. 407 if g.HasClientCert() { 408 var certFile, keyFile *os.File 409 410 // We need to actually create two temp files, one for storing cert data and 411 // another for storing the key. If we fail to create second fail, the first 412 // must be removed. 413 certFile, err := os.CreateTemp(argoio.TempDir, "") 414 if err != nil { 415 return NopCloser{}, nil, err 416 } 417 defer certFile.Close() 418 keyFile, err = os.CreateTemp(argoio.TempDir, "") 419 if err != nil { 420 removeErr := os.Remove(certFile.Name()) 421 if removeErr != nil { 422 log.Errorf("Could not remove previously created tempfile %s: %v", certFile.Name(), removeErr) 423 } 424 return NopCloser{}, nil, err 425 } 426 defer keyFile.Close() 427 428 // We should have both temp files by now 429 httpCloser = authFilePaths([]string{certFile.Name(), keyFile.Name()}) 430 431 _, err = certFile.WriteString(g.clientCertData) 432 if err != nil { 433 httpCloser.Close() 434 return NopCloser{}, nil, err 435 } 436 // GIT_SSL_CERT is the full path to a client certificate to be used 437 env = append(env, "GIT_SSL_CERT="+certFile.Name()) 438 439 _, err = keyFile.WriteString(g.clientCertKey) 440 if err != nil { 441 httpCloser.Close() 442 return NopCloser{}, nil, err 443 } 444 // GIT_SSL_KEY is the full path to a client certificate's key to be used 445 env = append(env, "GIT_SSL_KEY="+keyFile.Name()) 446 } 447 nonce := g.store.Add(githubAccessTokenUsername, token) 448 env = append(env, g.store.Environ(nonce)...) 449 return utilio.NewCloser(func() error { 450 g.store.Remove(nonce) 451 return httpCloser.Close() 452 }), env, nil 453 } 454 455 // GetUserInfo returns the username and email address for the credentials, if they're available. 456 func (g GitHubAppCreds) GetUserInfo(ctx context.Context) (string, string, error) { 457 // We use the apps transport to get the app slug. 458 appTransport, err := g.getAppTransport() 459 if err != nil { 460 return "", "", fmt.Errorf("failed to create GitHub app transport: %w", err) 461 } 462 appClient := github.NewClient(&http.Client{Transport: appTransport}) 463 app, _, err := appClient.Apps.Get(ctx, "") 464 if err != nil { 465 return "", "", fmt.Errorf("failed to get app info: %w", err) 466 } 467 468 // Then we use the installation transport to get the installation info. 469 appInstallTransport, err := g.getInstallationTransport() 470 if err != nil { 471 return "", "", fmt.Errorf("failed to get app installation: %w", err) 472 } 473 httpClient := http.Client{Transport: appInstallTransport} 474 client := github.NewClient(&httpClient) 475 476 appLogin := app.GetSlug() + "[bot]" 477 user, _, err := client.Users.Get(ctx, appLogin) 478 if err != nil { 479 return "", "", fmt.Errorf("failed to get app user info: %w", err) 480 } 481 authorName := user.GetLogin() 482 authorEmail := fmt.Sprintf("%d+%s@users.noreply.github.com", user.GetID(), user.GetLogin()) 483 return authorName, authorEmail, nil 484 } 485 486 // getAccessToken fetches GitHub token using the app id, install id, and private key. 487 // the token is then cached for re-use. 488 func (g GitHubAppCreds) getAccessToken() (string, error) { 489 // Timeout 490 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 491 defer cancel() 492 493 itr, err := g.getInstallationTransport() 494 if err != nil { 495 return "", fmt.Errorf("failed to create GitHub app installation transport: %w", err) 496 } 497 498 return itr.Token(ctx) 499 } 500 501 // getAppTransport creates a new GitHub transport for the app 502 func (g GitHubAppCreds) getAppTransport() (*ghinstallation.AppsTransport, error) { 503 // GitHub API url 504 baseURL := "https://api.github.com" 505 if g.baseURL != "" { 506 baseURL = strings.TrimSuffix(g.baseURL, "/") 507 } 508 509 // Create a new GitHub transport 510 c := GetRepoHTTPClient(baseURL, g.insecure, g, g.proxy, g.noProxy) 511 itr, err := ghinstallation.NewAppsTransport(c.Transport, 512 g.appID, 513 []byte(g.privateKey), 514 ) 515 if err != nil { 516 return nil, fmt.Errorf("failed to initialize GitHub installation transport: %w", err) 517 } 518 519 itr.BaseURL = baseURL 520 521 return itr, nil 522 } 523 524 // getInstallationTransport creates a new GitHub transport for the app installation 525 func (g GitHubAppCreds) getInstallationTransport() (*ghinstallation.Transport, error) { 526 // Compute hash of creds for lookup in cache 527 h := sha256.New() 528 _, err := fmt.Fprintf(h, "%s %d %d %s", g.privateKey, g.appID, g.appInstallId, g.baseURL) 529 if err != nil { 530 return nil, fmt.Errorf("failed to get get SHA256 hash for GitHub app credentials: %w", err) 531 } 532 key := hex.EncodeToString(h.Sum(nil)) 533 534 // Check cache for GitHub transport which helps fetch an API token 535 t, found := githubAppTokenCache.Get(key) 536 if found { 537 itr := t.(*ghinstallation.Transport) 538 // This method caches the token and if it's expired retrieves a new one 539 return itr, nil 540 } 541 542 // GitHub API url 543 baseURL := "https://api.github.com" 544 if g.baseURL != "" { 545 baseURL = strings.TrimSuffix(g.baseURL, "/") 546 } 547 548 // Create a new GitHub transport 549 c := GetRepoHTTPClient(baseURL, g.insecure, g, g.proxy, g.noProxy) 550 itr, err := ghinstallation.New(c.Transport, 551 g.appID, 552 g.appInstallId, 553 []byte(g.privateKey), 554 ) 555 if err != nil { 556 return nil, fmt.Errorf("failed to initialize GitHub installation transport: %w", err) 557 } 558 559 itr.BaseURL = baseURL 560 561 // Add transport to cache 562 githubAppTokenCache.Set(key, itr, time.Minute*60) 563 564 return itr, nil 565 } 566 567 func (g GitHubAppCreds) HasClientCert() bool { 568 return g.clientCertData != "" && g.clientCertKey != "" 569 } 570 571 func (g GitHubAppCreds) GetClientCertData() string { 572 return g.clientCertData 573 } 574 575 func (g GitHubAppCreds) GetClientCertKey() string { 576 return g.clientCertKey 577 } 578 579 var _ Creds = GoogleCloudCreds{} 580 581 // GoogleCloudCreds to authenticate to Google Cloud Source repositories 582 type GoogleCloudCreds struct { 583 creds *google.Credentials 584 store CredsStore 585 } 586 587 func NewGoogleCloudCreds(jsonData string, store CredsStore) GoogleCloudCreds { 588 creds, err := google.CredentialsFromJSON(context.Background(), []byte(jsonData), "https://www.googleapis.com/auth/cloud-platform") 589 if err != nil { 590 // Invalid JSON 591 log.Errorf("Failed reading credentials from JSON: %+v", err) 592 } 593 return GoogleCloudCreds{creds, store} 594 } 595 596 // GetUserInfo returns the username and email address for the credentials, if they're available. 597 // TODO: implement getting email instead of just username. 598 func (c GoogleCloudCreds) GetUserInfo(_ context.Context) (string, string, error) { 599 username, err := c.getUsername() 600 if err != nil { 601 return "", "", fmt.Errorf("failed to get username from creds: %w", err) 602 } 603 return username, "", nil 604 } 605 606 func (c GoogleCloudCreds) Environ() (io.Closer, []string, error) { 607 username, err := c.getUsername() 608 if err != nil { 609 return NopCloser{}, nil, fmt.Errorf("failed to get username from creds: %w", err) 610 } 611 token, err := c.getAccessToken() 612 if err != nil { 613 return NopCloser{}, nil, fmt.Errorf("failed to get access token from creds: %w", err) 614 } 615 616 nonce := c.store.Add(username, token) 617 env := c.store.Environ(nonce) 618 619 return utilio.NewCloser(func() error { 620 c.store.Remove(nonce) 621 return NopCloser{}.Close() 622 }), env, nil 623 } 624 625 func (c GoogleCloudCreds) getUsername() (string, error) { 626 type googleCredentialsFile struct { 627 Type string `json:"type"` 628 629 // Service Account fields 630 ClientEmail string `json:"client_email"` 631 PrivateKeyID string `json:"private_key_id"` 632 PrivateKey string `json:"private_key"` 633 AuthURL string `json:"auth_uri"` 634 TokenURL string `json:"token_uri"` 635 ProjectID string `json:"project_id"` 636 } 637 638 if c.creds == nil { 639 return "", errors.New("credentials for Google Cloud Source repositories are invalid") 640 } 641 642 var f googleCredentialsFile 643 if err := json.Unmarshal(c.creds.JSON, &f); err != nil { 644 return "", fmt.Errorf("failed to unmarshal Google Cloud credentials: %w", err) 645 } 646 return f.ClientEmail, nil 647 } 648 649 func (c GoogleCloudCreds) getAccessToken() (string, error) { 650 if c.creds == nil { 651 return "", errors.New("credentials for Google Cloud Source repositories are invalid") 652 } 653 654 // Compute hash of creds for lookup in cache 655 h := sha256.New() 656 _, err := h.Write(c.creds.JSON) 657 if err != nil { 658 return "", err 659 } 660 key := hex.EncodeToString(h.Sum(nil)) 661 662 t, found := googleCloudTokenSource.Get(key) 663 if found { 664 ts := t.(*oauth2.TokenSource) 665 token, err := (*ts).Token() 666 if err != nil { 667 return "", fmt.Errorf("failed to get token from Google Cloud token source: %w", err) 668 } 669 return token.AccessToken, nil 670 } 671 672 ts := c.creds.TokenSource 673 674 // Add TokenSource to cache 675 // As TokenSource handles refreshing tokens once they expire itself, TokenSource itself can be reused. Hence, no expiration. 676 googleCloudTokenSource.Set(key, &ts, gocache.NoExpiration) 677 678 token, err := ts.Token() 679 if err != nil { 680 return "", fmt.Errorf("failed to get get SHA256 hash for Google Cloud credentials: %w", err) 681 } 682 683 return token.AccessToken, nil 684 } 685 686 var _ Creds = AzureWorkloadIdentityCreds{} 687 688 type AzureWorkloadIdentityCreds struct { 689 store CredsStore 690 tokenProvider workloadidentity.TokenProvider 691 } 692 693 func NewAzureWorkloadIdentityCreds(store CredsStore, tokenProvider workloadidentity.TokenProvider) AzureWorkloadIdentityCreds { 694 return AzureWorkloadIdentityCreds{ 695 store: store, 696 tokenProvider: tokenProvider, 697 } 698 } 699 700 // GetUserInfo returns the username and email address for the credentials, if they're available. 701 func (creds AzureWorkloadIdentityCreds) GetUserInfo(_ context.Context) (string, string, error) { 702 // Email not implemented for HTTPS creds. 703 return workloadidentity.EmptyGuid, "", nil 704 } 705 706 func (creds AzureWorkloadIdentityCreds) Environ() (io.Closer, []string, error) { 707 token, err := creds.GetAzureDevOpsAccessToken() 708 if err != nil { 709 return NopCloser{}, nil, err 710 } 711 nonce := creds.store.Add("", token) 712 env := creds.store.Environ(nonce) 713 env = append(env, fmt.Sprintf("%s=Authorization: Bearer %s", bearerAuthHeaderEnv, token)) 714 715 return utilio.NewCloser(func() error { 716 creds.store.Remove(nonce) 717 return nil 718 }), env, nil 719 } 720 721 func (creds AzureWorkloadIdentityCreds) getAccessToken(scope string) (string, error) { 722 // Compute hash of creds for lookup in cache 723 key, err := argoutils.GenerateCacheKey("%s", scope) 724 if err != nil { 725 return "", fmt.Errorf("failed to get get SHA256 hash for Azure credentials: %w", err) 726 } 727 728 t, found := azureTokenCache.Get(key) 729 if found { 730 return t.(*workloadidentity.Token).AccessToken, nil 731 } 732 733 token, err := creds.tokenProvider.GetToken(scope) 734 if err != nil { 735 return "", fmt.Errorf("failed to get Azure access token: %w", err) 736 } 737 738 cacheExpiry := workloadidentity.CalculateCacheExpiryBasedOnTokenExpiry(token.ExpiresOn) 739 if cacheExpiry > 0 { 740 azureTokenCache.Set(key, token, cacheExpiry) 741 } 742 return token.AccessToken, nil 743 } 744 745 func (creds AzureWorkloadIdentityCreds) GetAzureDevOpsAccessToken() (string, error) { 746 accessToken, err := creds.getAccessToken(azureDevopsEntraResourceId) // wellknown resourceid of Azure DevOps 747 return accessToken, err 748 }