diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 54cca4e8..702c11b0 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -357,8 +357,14 @@ func buildGitSecret(keypair *ssh.KeyPair, hostKey []byte, options Options) (secr secret.Labels = options.Labels secret.StringData = map[string]string{} - if options.Username != "" && options.Password != "" { + // Username and Password are written independently so that callers can + // create a password-only secret (e.g. an Azure DevOps PAT, where the + // username is unused). When a keypair is also present, Password acts as + // the SSH private-key passphrase. + if options.Username != "" { secret.StringData[UsernameSecretKey] = options.Username + } + if options.Password != "" { secret.StringData[PasswordSecretKey] = options.Password } if options.BearerToken != "" { @@ -374,10 +380,6 @@ func buildGitSecret(keypair *ssh.KeyPair, hostKey []byte, options Options) (secr secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) - // set password if present - if options.Password != "" { - secret.StringData[PasswordSecretKey] = string(options.Password) - } } return secret diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go index 8eb619d4..d57a3aad 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -88,3 +88,67 @@ func Test_PasswordlessLoadKeyPair(t *testing.T) { }) } } + +// Test_buildGitSecret_BasicAuthFields covers the regression reported in +// https://github.com/fluxcd/flux2/issues/3892: providing just --password +// (e.g. an Azure DevOps PAT, which ignores the username) must still +// produce a secret containing the password field. +func Test_buildGitSecret_BasicAuthFields(t *testing.T) { + tests := []struct { + name string + opts Options + wantUsername string + wantPassword string + wantHasUser bool + wantHasPass bool + }{ + { + name: "username and password", + opts: Options{Username: "git", Password: "pw"}, + wantUsername: "git", + wantPassword: "pw", + wantHasUser: true, + wantHasPass: true, + }, + { + name: "password only (Azure DevOps PAT)", + opts: Options{Password: "pat-token"}, + wantPassword: "pat-token", + wantHasUser: false, + wantHasPass: true, + }, + { + name: "username only", + opts: Options{Username: "git"}, + wantUsername: "git", + wantHasUser: true, + wantHasPass: false, + }, + { + name: "no credentials", + opts: Options{}, + wantHasUser: false, + wantHasPass: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + secret := buildGitSecret(nil, nil, tt.opts) + gotUser, hasUser := secret.StringData[UsernameSecretKey] + gotPass, hasPass := secret.StringData[PasswordSecretKey] + if hasUser != tt.wantHasUser { + t.Errorf("username presence = %v, want %v", hasUser, tt.wantHasUser) + } + if hasPass != tt.wantHasPass { + t.Errorf("password presence = %v, want %v", hasPass, tt.wantHasPass) + } + if tt.wantHasUser && gotUser != tt.wantUsername { + t.Errorf("username = %q, want %q", gotUser, tt.wantUsername) + } + if tt.wantHasPass && gotPass != tt.wantPassword { + t.Errorf("password = %q, want %q", gotPass, tt.wantPassword) + } + }) + } +}