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  }