Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support non-TTY Stdin for Confirmation prompts #127

Merged
merged 1 commit into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Nothing should go in this section, please add to the latest unreleased version
(and update the corresponding date), or add a new version.

## [8.0.8] - 2023-04-19

### Fixed
- Fixed piping input to `conjur init` confirmation prompts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be worth checking with @szh if he will be re-releasing v8.0.7 with this fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I released 8.0.7 yesterday but am hoping to squeeze in 8.0.0 if I can before the end of the week

[cyberark/conjur-cli-go#127](https://github.com/cyberark/conjur-cli-go/pull/127)

## [8.0.7] - 2023-04-18

### Fixed
Expand Down Expand Up @@ -76,7 +82,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added
- Placeholder version to capture the reset of the repository

[Unreleased]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.7...HEAD
[Unreleased]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.8...HEAD
[8.0.8]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.7...v8.0.8
[8.0.7]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.6...v8.0.7
[8.0.6]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.5...v8.0.6
[8.0.5]: https://github.com/cyberark/conjur-cli-go/compare/v8.0.4...v8.0.5
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/creack/pty v1.1.18
github.com/cyberark/conjur-api-go v0.10.3-0.20230217154521-e01f713716d2 // Run "go get github.com/cyberark/conjur-api-go@main" to update
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02
github.com/mattn/go-isatty v0.0.8
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.1
Expand All @@ -27,7 +28,6 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
Expand Down
25 changes: 22 additions & 3 deletions pkg/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ var initCmdTestCases = []struct {
args []string
out string
promptResponses []promptResponse
beforeTest func(t *testing.T, conjurrcInTmpDir string)
assert func(t *testing.T, conjurrcInTmpDir string, stdout string)
// Being unable to pipe responses to prompts is a known shortcoming of Survey.
// https://github.com/go-survey/survey/issues/394
// This flag is used to enable Pipe-based, and not PTY-based, tests.
pipe bool
beforeTest func(t *testing.T, conjurrcInTmpDir string)
assert func(t *testing.T, conjurrcInTmpDir string, stdout string)
}{
{
name: "help",
Expand Down Expand Up @@ -89,6 +93,7 @@ appliance_url: http://conjur
response: "N",
},
},
pipe: true,
beforeTest: func(t *testing.T, conjurrcInTmpDir string) {
os.WriteFile(conjurrcInTmpDir, []byte("something"), 0644)
},
Expand All @@ -107,6 +112,7 @@ appliance_url: http://conjur
response: "y",
},
},
pipe: true,
beforeTest: func(t *testing.T, conjurrcInTmpDir string) {
os.WriteFile(conjurrcInTmpDir, []byte("something"), 0644)
},
Expand Down Expand Up @@ -168,6 +174,7 @@ appliance_url: http://host
response: "y",
},
},
pipe: true,
assert: func(t *testing.T, conjurrcInTmpDir string, stdout string) {
assertCertWritten(t, conjurrcInTmpDir, stdout)
},
Expand All @@ -181,6 +188,7 @@ appliance_url: http://host
response: "N",
},
},
pipe: true,
assert: func(t *testing.T, conjurrcInTmpDir string, stdout string) {
fmt.Println(stdout)
assert.Contains(t, stdout, "You decided not to trust the certificate")
Expand All @@ -205,13 +213,15 @@ appliance_url: http://host
},
},
{
name: "succeeds for self-signed certificate with --self-signed flag",
args: []string{"init", "-u=https://self-signed.badssl.com", "-a=test-account", "--self-signed"},
promptResponses: []promptResponse{
{
prompt: "Trust this certificate?",
response: "y",
},
},
pipe: true,
assert: func(t *testing.T, conjurrcInTmpDir string, stdout string) {
assert.Contains(t, stdout, "Warning: Using self-signed certificates is not recommended and could lead to exposure of sensitive data")
assertCertWritten(t, conjurrcInTmpDir, stdout)
Expand Down Expand Up @@ -313,7 +323,16 @@ func TestInitCmd(t *testing.T) {
rootCmd.AddCommand(cmd)
rootCmd.SetArgs(args)

out, _ := executeCommandForTestWithPromptResponses(t, rootCmd, tc.promptResponses)
var out string
if tc.pipe {
var content string
for _, pr := range tc.promptResponses {
content = fmt.Sprintf("%s%s\n", content, pr.response)
}
out, _ = executeCommandForTestWithPipeResponses(t, rootCmd, content)
} else {
out, _ = executeCommandForTestWithPromptResponses(t, rootCmd, tc.promptResponses)
}

if tc.assert != nil {
tc.assert(t, conjurrcInTmpDir, out)
Expand Down
44 changes: 43 additions & 1 deletion pkg/cmd/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"bytes"
"io"
"io/ioutil"
"os"
"regexp"
"testing"
Expand Down Expand Up @@ -45,10 +46,51 @@ func executeCommandForTest(t *testing.T, c *cobra.Command, args ...string) (stri
cmd.SetIn(bytes.NewReader(nil))
err := cmd.Execute()

// strip ansi from stdout and stderr because we're using promptui
return stripAnsi(stdoutBuf.String()), stripAnsi(stderrBuf.String()), err
}

func executeCommandForTestWithPipeResponses(
t *testing.T, cmd *cobra.Command, content string,
) (stdOut string, cmdErr error) {
t.Helper()

mockHelpText(cmd)

// Send CLI stdOut and stdErr to Pipes and consolidate
outR, outW, _ := os.Pipe()
errR, errW, _ := os.Pipe()
combinedOutReader := io.MultiReader(outR, errR)

defer func(origOut *os.File) { os.Stdout = origOut }(os.Stdout)
defer func(origErr *os.File) { os.Stderr = origErr }(os.Stderr)
os.Stdout = outW
os.Stderr = errW

// Receive CLI stdIn from Pipe
inR, inW, _ := os.Pipe()
_, err := inW.Write([]byte(content))
if err != nil {
t.Fatal(err)
}
inW.Close()

defer func(origIn *os.File) { os.Stdin = origIn }(os.Stdin)
os.Stdin = inR

// Run CLI cmd
err = cmd.Execute()

// Collect stdOut
outW.Close()
errW.Close()
out, outErr := ioutil.ReadAll(combinedOutReader)
if outErr != nil {
t.Fatal(outErr)
}

return stripAnsi(string(out)), err
}

func executeCommandForTestWithPromptResponses(
t *testing.T, cmd *cobra.Command, promptResponses []promptResponse,
) (stdOut string, cmdErr error) {
Expand Down
63 changes: 42 additions & 21 deletions pkg/prompts/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"net/url"
"os"
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)

Expand All @@ -32,46 +34,65 @@ func newAccountPrompt() *survey.Question {
}
}

func newFileExistsPrompt(filePath string) *survey.Question {
return &survey.Question{
Prompt: &survey.Confirm{Message: fmt.Sprintf("File %s exists. Overwrite?", filePath)},
Validate: survey.Required,
func stringToBool(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "y":
return true
case "yes":
return true
default:
return false
}
}

// AskToOverwriteFile presents a prompt to get confirmation from a user to overwrite a file
func AskToOverwriteFile(filePath string) error {
var userInput bool
func basicConfirm(message string) bool {
fmt.Fprintf(os.Stdout, "%s ", message)
var answer string
fmt.Scanln(&answer)
fmt.Fprintln(os.Stdout, "")
return stringToBool(answer)
}

q := newFileExistsPrompt(filePath)
err := survey.AskOne(q.Prompt, &userInput, survey.WithValidator(q.Validate), survey.WithShowCursor(true))
func confirm(message string) (bool, error) {
var err error
var userInput bool

if userInput == false {
return fmt.Errorf("Not overwriting %s", filePath)
// When handling Pipe-based Stdin, we can't use Survey to gather responses.
// https://github.com/go-survey/survey/issues/394
// Instead, use standard fmt funcs to prompt to Stdout and gather from Stdin.
if isatty.IsTerminal(os.Stdin.Fd()) {
q := &survey.Question{
Prompt: &survey.Confirm{Message: message},
Validate: survey.Required,
}
err = survey.AskOne(q.Prompt, &userInput, survey.WithValidator(q.Validate), survey.WithShowCursor(true))
} else {
userInput = basicConfirm(message)
}
return err

return userInput, err
}

func newTrustCertPrompt() *survey.Question {
return &survey.Question{
Prompt: &survey.Confirm{Message: "Trust this certificate?"},
Validate: survey.Required,
// AskToOverwriteFile presents a prompt to get confirmation from a user to overwrite a file
func AskToOverwriteFile(filePath string) error {
userInput, err := confirm(fmt.Sprintf("File %s exists. Overwrite?", filePath))

if !userInput {
return fmt.Errorf("Not overwriting %s", filePath)
}
return err
}

// AskToTrustCert presents a prompt to get confirmation from a user to trust a certificate
func AskToTrustCert(fingerprint string) error {
var userInput bool

warning := fmt.Sprintf("\nThe server's certificate fingerprint is %s.\n", fingerprint) +
"Please verify this certificate on the appliance using command:\n" +
"openssl x509 -fingerprint -noout -in ~conjur/etc/ssl/conjur.pem\n\n"
os.Stdout.Write([]byte(warning))

q := newTrustCertPrompt()
err := survey.AskOne(q.Prompt, &userInput, survey.WithValidator(q.Validate), survey.WithShowCursor(true))
userInput, err := confirm("Trust this certificate?")

if userInput == false {
if !userInput {
return errors.New("You decided not to trust the certificate.")
}
return err
Expand Down