github.com/argoproj/argo-cd/v3@v3.2.1/util/git/creds_test.go (about) 1 package git 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "os" 7 "path" 8 "regexp" 9 "strings" 10 "testing" 11 "time" 12 13 "github.com/google/uuid" 14 gocache "github.com/patrickmn/go-cache" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 "golang.org/x/oauth2" 18 "golang.org/x/oauth2/google" 19 20 argoio "github.com/argoproj/gitops-engine/pkg/utils/io" 21 22 "github.com/argoproj/argo-cd/v3/util/cert" 23 utilio "github.com/argoproj/argo-cd/v3/util/io" 24 "github.com/argoproj/argo-cd/v3/util/workloadidentity" 25 "github.com/argoproj/argo-cd/v3/util/workloadidentity/mocks" 26 ) 27 28 type cred struct { 29 username string 30 password string 31 } 32 33 type memoryCredsStore struct { 34 creds map[string]cred 35 } 36 37 func (s *memoryCredsStore) Add(username string, password string) string { 38 id := uuid.New().String() 39 s.creds[id] = cred{ 40 username: username, 41 password: password, 42 } 43 return id 44 } 45 46 func (s *memoryCredsStore) Remove(id string) { 47 delete(s.creds, id) 48 } 49 50 func (s *memoryCredsStore) Environ(_ string) []string { 51 return nil 52 } 53 54 func TestHTTPSCreds_Environ_no_cert_cleanup(t *testing.T) { 55 store := &memoryCredsStore{creds: make(map[string]cred)} 56 creds := NewHTTPSCreds("", "", "", "", "", true, store, false) 57 closer, _, err := creds.Environ() 58 require.NoError(t, err) 59 credsLenBefore := len(store.creds) 60 utilio.Close(closer) 61 assert.Len(t, store.creds, credsLenBefore-1) 62 } 63 64 func TestHTTPSCreds_Environ_insecure_true(t *testing.T) { 65 creds := NewHTTPSCreds("", "", "", "", "", true, &NoopCredsStore{}, false) 66 closer, env, err := creds.Environ() 67 t.Cleanup(func() { 68 utilio.Close(closer) 69 }) 70 require.NoError(t, err) 71 found := false 72 for _, envVar := range env { 73 if envVar == "GIT_SSL_NO_VERIFY=true" { 74 found = true 75 break 76 } 77 } 78 assert.True(t, found) 79 } 80 81 func TestHTTPSCreds_Environ_insecure_false(t *testing.T) { 82 creds := NewHTTPSCreds("", "", "", "", "", false, &NoopCredsStore{}, false) 83 closer, env, err := creds.Environ() 84 t.Cleanup(func() { 85 utilio.Close(closer) 86 }) 87 require.NoError(t, err) 88 found := false 89 for _, envVar := range env { 90 if envVar == "GIT_SSL_NO_VERIFY=true" { 91 found = true 92 break 93 } 94 } 95 assert.False(t, found) 96 } 97 98 func TestHTTPSCreds_Environ_forceBasicAuth(t *testing.T) { 99 t.Run("Enabled and credentials set", func(t *testing.T) { 100 store := &memoryCredsStore{creds: make(map[string]cred)} 101 creds := NewHTTPSCreds("username", "password", "", "", "", false, store, true) 102 closer, env, err := creds.Environ() 103 require.NoError(t, err) 104 defer closer.Close() 105 var header string 106 for _, envVar := range env { 107 if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") { 108 header = envVar[len(forceBasicAuthHeaderEnv)+1:] 109 } 110 if header != "" { 111 break 112 } 113 } 114 b64enc := base64.StdEncoding.EncodeToString([]byte("username:password")) 115 assert.Equal(t, "Authorization: Basic "+b64enc, header) 116 }) 117 t.Run("Enabled but credentials not set", func(t *testing.T) { 118 store := &memoryCredsStore{creds: make(map[string]cred)} 119 creds := NewHTTPSCreds("", "", "", "", "", false, store, true) 120 closer, env, err := creds.Environ() 121 require.NoError(t, err) 122 defer closer.Close() 123 var header string 124 for _, envVar := range env { 125 if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") { 126 header = envVar[len(forceBasicAuthHeaderEnv)+1:] 127 } 128 if header != "" { 129 break 130 } 131 } 132 assert.Empty(t, header) 133 }) 134 t.Run("Disabled with credentials set", func(t *testing.T) { 135 store := &memoryCredsStore{creds: make(map[string]cred)} 136 creds := NewHTTPSCreds("username", "password", "", "", "", false, store, false) 137 closer, env, err := creds.Environ() 138 require.NoError(t, err) 139 defer closer.Close() 140 var header string 141 for _, envVar := range env { 142 if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") { 143 header = envVar[len(forceBasicAuthHeaderEnv)+1:] 144 } 145 if header != "" { 146 break 147 } 148 } 149 assert.Empty(t, header) 150 }) 151 152 t.Run("Disabled with credentials not set", func(t *testing.T) { 153 store := &memoryCredsStore{creds: make(map[string]cred)} 154 creds := NewHTTPSCreds("", "", "", "", "", false, store, false) 155 closer, env, err := creds.Environ() 156 require.NoError(t, err) 157 defer closer.Close() 158 var header string 159 for _, envVar := range env { 160 if strings.HasPrefix(envVar, forceBasicAuthHeaderEnv+"=") { 161 header = envVar[len(forceBasicAuthHeaderEnv)+1:] 162 } 163 if header != "" { 164 break 165 } 166 } 167 assert.Empty(t, header) 168 }) 169 } 170 171 func TestHTTPSCreds_Environ_bearerTokenAuth(t *testing.T) { 172 t.Run("Enabled and credentials set", func(t *testing.T) { 173 store := &memoryCredsStore{creds: make(map[string]cred)} 174 creds := NewHTTPSCreds("", "", "token", "", "", false, store, false) 175 closer, env, err := creds.Environ() 176 require.NoError(t, err) 177 defer closer.Close() 178 var header string 179 for _, envVar := range env { 180 if strings.HasPrefix(envVar, bearerAuthHeaderEnv+"=") { 181 header = envVar[len(bearerAuthHeaderEnv)+1:] 182 } 183 if header != "" { 184 break 185 } 186 } 187 assert.Equal(t, "Authorization: Bearer token", header) 188 }) 189 } 190 191 func TestHTTPSCreds_Environ_clientCert(t *testing.T) { 192 store := &memoryCredsStore{creds: make(map[string]cred)} 193 creds := NewHTTPSCreds("", "", "", "clientCertData", "clientCertKey", false, store, false) 194 closer, env, err := creds.Environ() 195 require.NoError(t, err) 196 var cert, key string 197 for _, envVar := range env { 198 if strings.HasPrefix(envVar, "GIT_SSL_CERT=") { 199 cert = envVar[13:] 200 } else if strings.HasPrefix(envVar, "GIT_SSL_KEY=") { 201 key = envVar[12:] 202 } 203 if cert != "" && key != "" { 204 break 205 } 206 } 207 assert.NotEmpty(t, cert) 208 assert.NotEmpty(t, key) 209 210 certBytes, err := os.ReadFile(cert) 211 require.NoError(t, err) 212 assert.Equal(t, "clientCertData", string(certBytes)) 213 keyBytes, err := os.ReadFile(key) 214 assert.Equal(t, "clientCertKey", string(keyBytes)) 215 require.NoError(t, err) 216 217 utilio.Close(closer) 218 219 _, err = os.Stat(cert) 220 require.ErrorIs(t, err, os.ErrNotExist) 221 _, err = os.Stat(key) 222 require.ErrorIs(t, err, os.ErrNotExist) 223 } 224 225 func Test_SSHCreds_Environ(t *testing.T) { 226 for _, insecureIgnoreHostKey := range []bool{false, true} { 227 tempDir := t.TempDir() 228 caFile := path.Join(tempDir, "caFile") 229 err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600)) 230 require.NoError(t, err) 231 creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "") 232 closer, env, err := creds.Environ() 233 require.NoError(t, err) 234 require.Len(t, env, 2) 235 236 assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set") 237 238 assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND=")) 239 240 if insecureIgnoreHostKey { 241 assert.Contains(t, env[1], "-o StrictHostKeyChecking=no") 242 assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null") 243 } else { 244 assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes") 245 hostsPath := cert.GetSSHKnownHostsDataPath() 246 assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath) 247 } 248 249 envRegex := regexp.MustCompile("-i ([^ ]+)") 250 assert.Regexp(t, envRegex, env[1]) 251 privateKeyFile := envRegex.FindStringSubmatch(env[1])[1] 252 assert.FileExists(t, privateKeyFile) 253 utilio.Close(closer) 254 assert.NoFileExists(t, privateKeyFile) 255 } 256 } 257 258 func Test_SSHCreds_Environ_WithProxy(t *testing.T) { 259 for _, insecureIgnoreHostKey := range []bool{false, true} { 260 tempDir := t.TempDir() 261 caFile := path.Join(tempDir, "caFile") 262 err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600)) 263 require.NoError(t, err) 264 creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "socks5://127.0.0.1:1080") 265 closer, env, err := creds.Environ() 266 require.NoError(t, err) 267 require.Len(t, env, 2) 268 269 assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set") 270 271 assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND=")) 272 273 if insecureIgnoreHostKey { 274 assert.Contains(t, env[1], "-o StrictHostKeyChecking=no") 275 assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null") 276 } else { 277 assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes") 278 hostsPath := cert.GetSSHKnownHostsDataPath() 279 assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath) 280 } 281 assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'") 282 283 envRegex := regexp.MustCompile("-i ([^ ]+)") 284 assert.Regexp(t, envRegex, env[1]) 285 privateKeyFile := envRegex.FindStringSubmatch(env[1])[1] 286 assert.FileExists(t, privateKeyFile) 287 utilio.Close(closer) 288 assert.NoFileExists(t, privateKeyFile) 289 } 290 } 291 292 func Test_SSHCreds_Environ_WithProxyUserNamePassword(t *testing.T) { 293 for _, insecureIgnoreHostKey := range []bool{false, true} { 294 tempDir := t.TempDir() 295 caFile := path.Join(tempDir, "caFile") 296 err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600)) 297 require.NoError(t, err) 298 creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, "socks5://user:password@127.0.0.1:1080") 299 closer, env, err := creds.Environ() 300 require.NoError(t, err) 301 require.Len(t, env, 4) 302 303 assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set") 304 305 assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND=")) 306 assert.Equal(t, "SOCKS5_USER=user", env[2], "SOCKS5 user env var must be set") 307 assert.Equal(t, "SOCKS5_PASSWD=password", env[3], "SOCKS5 password env var must be set") 308 309 if insecureIgnoreHostKey { 310 assert.Contains(t, env[1], "-o StrictHostKeyChecking=no") 311 assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null") 312 } else { 313 assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes") 314 hostsPath := cert.GetSSHKnownHostsDataPath() 315 assert.Contains(t, env[1], "-o UserKnownHostsFile="+hostsPath) 316 } 317 assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'") 318 319 envRegex := regexp.MustCompile("-i ([^ ]+)") 320 assert.Regexp(t, envRegex, env[1]) 321 privateKeyFile := envRegex.FindStringSubmatch(env[1])[1] 322 assert.FileExists(t, privateKeyFile) 323 utilio.Close(closer) 324 assert.NoFileExists(t, privateKeyFile) 325 } 326 } 327 328 func Test_SSHCreds_Environ_TempFileCleanupOnInvalidProxyURL(t *testing.T) { 329 // Previously, if the proxy URL was invalid, a temporary file would be left in /dev/shm. This ensures the file is cleaned up in this case. 330 331 // argoio.TempDir will be /dev/shm or "" (on an OS without /dev/shm). 332 // In this case os.CreateTemp(), which is used by creds.Environ(), 333 // will use os.TempDir for the temporary directory. 334 // Reproducing this logic here: 335 argoioTempDir := argoio.TempDir 336 if argoioTempDir == "" { 337 argoioTempDir = os.TempDir() 338 } 339 340 // countDev returns the number of files in the temporary directory 341 countFilesInDevShm := func() int { 342 entries, err := os.ReadDir(argoioTempDir) 343 require.NoError(t, err) 344 345 return len(entries) 346 } 347 348 for _, insecureIgnoreHostKey := range []bool{false, true} { 349 tempDir := t.TempDir() 350 caFile := path.Join(tempDir, "caFile") 351 err := os.WriteFile(caFile, []byte(""), os.FileMode(0o600)) 352 require.NoError(t, err) 353 creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, ":invalid-proxy-url") 354 355 filesInDevShmBeforeInvocation := countFilesInDevShm() 356 357 _, _, err = creds.Environ() 358 require.Error(t, err) 359 360 filesInDevShmAfterInvocation := countFilesInDevShm() 361 362 assert.Equal(t, filesInDevShmBeforeInvocation, filesInDevShmAfterInvocation, "no temporary files should leak if the proxy url cannot be parsed") 363 } 364 } 365 366 const gcpServiceAccountKeyJSON = `{ 367 "type": "service_account", 368 "project_id": "my-google-project", 369 "private_key_id": "REDACTED", 370 "private_key": "-----BEGIN PRIVATE KEY-----\nREDACTED\n-----END PRIVATE KEY-----\n", 371 "client_email": "argocd-service-account@my-google-project.iam.gserviceaccount.com", 372 "client_id": "REDACTED", 373 "auth_uri": "https://accounts.google.com/o/oauth2/auth", 374 "token_uri": "https://oauth2.googleapis.com/token", 375 "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 376 "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/argocd-service-account%40my-google-project.iam.gserviceaccount.com" 377 }` 378 379 const invalidJSON = `{ 380 "type": "service_account", 381 "project_id": "my-google-project", 382 ` 383 384 func TestNewGoogleCloudCreds(t *testing.T) { 385 store := &memoryCredsStore{creds: make(map[string]cred)} 386 googleCloudCreds := NewGoogleCloudCreds(gcpServiceAccountKeyJSON, store) 387 assert.NotNil(t, googleCloudCreds) 388 } 389 390 func TestNewGoogleCloudCreds_invalidJSON(t *testing.T) { 391 store := &memoryCredsStore{creds: make(map[string]cred)} 392 googleCloudCreds := NewGoogleCloudCreds(invalidJSON, store) 393 assert.Nil(t, googleCloudCreds.creds) 394 395 token, err := googleCloudCreds.getAccessToken() 396 assert.Empty(t, token) 397 require.Error(t, err) 398 399 username, err := googleCloudCreds.getUsername() 400 assert.Empty(t, username) 401 require.Error(t, err) 402 403 closer, envStringSlice, err := googleCloudCreds.Environ() 404 assert.Equal(t, NopCloser{}, closer) 405 assert.Equal(t, []string(nil), envStringSlice) 406 require.Error(t, err) 407 } 408 409 func TestGoogleCloudCreds_Environ_cleanup(t *testing.T) { 410 store := &memoryCredsStore{creds: make(map[string]cred)} 411 staticToken := &oauth2.Token{AccessToken: "token"} 412 googleCloudCreds := GoogleCloudCreds{&google.Credentials{ 413 ProjectID: "my-google-project", 414 TokenSource: oauth2.StaticTokenSource(staticToken), 415 JSON: []byte(gcpServiceAccountKeyJSON), 416 }, store} 417 418 closer, _, err := googleCloudCreds.Environ() 419 require.NoError(t, err) 420 credsLenBefore := len(store.creds) 421 utilio.Close(closer) 422 assert.Len(t, store.creds, credsLenBefore-1) 423 } 424 425 func TestAzureWorkloadIdentityCreds_Environ(t *testing.T) { 426 resetAzureTokenCache() 427 store := &memoryCredsStore{creds: make(map[string]cred)} 428 workloadIdentityMock := new(mocks.TokenProvider) 429 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil) 430 creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock} 431 _, env, err := creds.Environ() 432 require.NoError(t, err) 433 assert.Len(t, store.creds, 1) 434 435 for _, value := range store.creds { 436 assert.Empty(t, value.username) 437 assert.Equal(t, "accessToken", value.password) 438 } 439 440 require.Len(t, env, 1) 441 assert.Equal(t, "ARGOCD_GIT_BEARER_AUTH_HEADER=Authorization: Bearer accessToken", env[0], "ARGOCD_GIT_BEARER_AUTH_HEADER env var must be set") 442 } 443 444 func TestAzureWorkloadIdentityCreds_Environ_cleanup(t *testing.T) { 445 resetAzureTokenCache() 446 store := &memoryCredsStore{creds: make(map[string]cred)} 447 workloadIdentityMock := new(mocks.TokenProvider) 448 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil) 449 creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock} 450 closer, _, err := creds.Environ() 451 require.NoError(t, err) 452 credsLenBefore := len(store.creds) 453 utilio.Close(closer) 454 assert.Len(t, store.creds, credsLenBefore-1) 455 } 456 457 func TestAzureWorkloadIdentityCreds_GetUserInfo(t *testing.T) { 458 resetAzureTokenCache() 459 store := &memoryCredsStore{creds: make(map[string]cred)} 460 workloadIdentityMock := new(mocks.TokenProvider) 461 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId).Return(&workloadidentity.Token{AccessToken: "accessToken", ExpiresOn: time.Now().Add(time.Minute)}, nil) 462 creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock} 463 464 user, email, err := creds.GetUserInfo(t.Context()) 465 require.NoError(t, err) 466 assert.Equal(t, workloadidentity.EmptyGuid, user) 467 assert.Empty(t, email) 468 } 469 470 func TestGetHelmCredsShouldReturnHelmCredsIfAzureWorkloadIdentityNotSpecified(t *testing.T) { 471 var creds Creds = NewAzureWorkloadIdentityCreds(NoopCredsStore{}, new(mocks.TokenProvider)) 472 473 _, ok := creds.(AzureWorkloadIdentityCreds) 474 require.Truef(t, ok, "expected HelmCreds but got %T", creds) 475 } 476 477 func TestAzureWorkloadIdentityCreds_FetchNewTokenIfExistingIsExpired(t *testing.T) { 478 resetAzureTokenCache() 479 store := &memoryCredsStore{creds: make(map[string]cred)} 480 workloadIdentityMock := new(mocks.TokenProvider) 481 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId). 482 Return(&workloadidentity.Token{AccessToken: "firstToken", ExpiresOn: time.Now().Add(time.Minute)}, nil).Once() 483 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId). 484 Return(&workloadidentity.Token{AccessToken: "secondToken"}, nil).Once() 485 creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock} 486 token, err := creds.GetAzureDevOpsAccessToken() 487 require.NoError(t, err) 488 489 assert.Equal(t, "firstToken", token) 490 time.Sleep(5 * time.Second) 491 token, err = creds.GetAzureDevOpsAccessToken() 492 require.NoError(t, err) 493 assert.Equal(t, "secondToken", token) 494 } 495 496 func TestAzureWorkloadIdentityCreds_ReuseTokenIfExistingIsNotExpired(t *testing.T) { 497 resetAzureTokenCache() 498 store := &memoryCredsStore{creds: make(map[string]cred)} 499 workloadIdentityMock := new(mocks.TokenProvider) 500 firstToken := &workloadidentity.Token{AccessToken: "firstToken", ExpiresOn: time.Now().Add(6 * time.Minute)} 501 secondToken := &workloadidentity.Token{AccessToken: "secondToken"} 502 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId).Return(firstToken, nil).Once() 503 workloadIdentityMock.On("GetToken", azureDevopsEntraResourceId).Return(secondToken, nil).Once() 504 creds := AzureWorkloadIdentityCreds{store, workloadIdentityMock} 505 token, err := creds.GetAzureDevOpsAccessToken() 506 require.NoError(t, err) 507 508 assert.Equal(t, "firstToken", token) 509 time.Sleep(5 * time.Second) 510 token, err = creds.GetAzureDevOpsAccessToken() 511 require.NoError(t, err) 512 assert.Equal(t, "firstToken", token) 513 } 514 515 func resetAzureTokenCache() { 516 azureTokenCache = gocache.New(gocache.NoExpiration, 0) 517 }