diff --git a/cmd/harbor/root/login.go b/cmd/harbor/root/login.go index 5b9906187..fd8b94abd 100644 --- a/cmd/harbor/root/login.go +++ b/cmd/harbor/root/login.go @@ -16,7 +16,6 @@ package root import ( "context" "fmt" - "os" "strings" "github.com/goharbor/go-client/pkg/harbor" @@ -28,7 +27,6 @@ import ( "github.com/goharbor/harbor-cli/pkg/views/login" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "golang.org/x/term" ) var ( @@ -53,13 +51,11 @@ func LoginCommand() *cobra.Command { } if passwordStdin { - fmt.Print("Password: ") - passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) // #nosec G115 - fd fits in int on all supported platforms + password, err := utils.GetSecretStdin("Password: ") if err != nil { return fmt.Errorf("failed to read password from stdin: %v", err) } - fmt.Println() - Password = string(passwordBytes) + Password = password } loginView := login.LoginView{ diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 3ab996771..c84f11496 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -18,11 +18,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "regexp" "strconv" "strings" - "syscall" "github.com/charmbracelet/bubbles/table" "github.com/gocarina/gocsv" @@ -187,13 +187,25 @@ func SavePayloadJSON(filename string, payload any) { // Get Password as Stdin func GetSecretStdin(prompt string) (string, error) { - fmt.Print(prompt) - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + if term.IsTerminal(int(os.Stdin.Fd())) { // #nosec G115 - fd fits in int on all supported platforms + fmt.Print(prompt) + bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) // #nosec G115 - fd fits in int on all supported platforms + if err != nil { + return "", err + } + fmt.Println() // move to the next line after input + return trimSecretLineEnding(bytePassword), nil + } + + bytePassword, err := io.ReadAll(os.Stdin) if err != nil { return "", err } - fmt.Println() // move to the next line after input - return strings.TrimSpace(string(bytePassword)), nil + return trimSecretLineEnding(bytePassword), nil +} + +func trimSecretLineEnding(secret []byte) string { + return strings.TrimRight(string(secret), "\r\n") } func ToKebabCase(s string) string { diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index b255f47d1..a6a87d61b 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -15,11 +15,13 @@ package utils_test import ( "fmt" + "os" "testing" "github.com/goharbor/harbor-cli/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_Sanitize_ServerAddress(t *testing.T) { @@ -145,3 +147,38 @@ func TestStorageStringToBytes(t *testing.T) { _, err := utils.StorageStringToBytes("1025TiB") assert.Error(t, err, "Expected error for input exceeding 1024TiB but got none") } + +func TestGetSecretStdinReadsPipedInput(t *testing.T) { + secret := getSecretFromPipe(t, "Abcd1234\n") + + assert.Equal(t, "Abcd1234", secret) +} + +func TestGetSecretStdinPreservesSecretWhitespace(t *testing.T) { + secret := getSecretFromPipe(t, " Abcd1234 \r\n") + + assert.Equal(t, " Abcd1234 ", secret) +} + +func getSecretFromPipe(t *testing.T, input string) string { + t.Helper() + + oldStdin := os.Stdin + reader, writer, err := os.Pipe() + require.NoError(t, err) + + _, err = writer.WriteString(input) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + os.Stdin = reader + t.Cleanup(func() { + os.Stdin = oldStdin + _ = reader.Close() + }) + + secret, err := utils.GetSecretStdin("Password: ") + require.NoError(t, err) + + return secret +}