Skip to content

Commit

Permalink
Backport lib/utils/prompt improvements to [v9] (#13822)
Browse files Browse the repository at this point in the history
`prompt.ContextReader`, along with various parts of `lib/utils/prompt`, where
re-written in master so we can alleviate input swallowing issues.

For various reasons I didn't backport all changes to v9, but the less-eager
input-swallowing loop is now what stands between us and issue #13021.

This isn't a backport of a specific PR, but instead a port of the entire
`lib/utils/prompt` package, as the PRs that touch the package unfortunately do
more than we want to backport. The interfaces are still compatible and I did
test various `tsh login` and `tsh ssh` scenarios.

I've thrown in #13382 for good measure, as well.

Closes #13021.

* Backport lib/utils/prompt improvements
* Fix lib/client tests
* Restore terminal state on exit (#13382)
  • Loading branch information
codingllama authored Jun 24, 2022
1 parent 535f448 commit 89f48f7
Show file tree
Hide file tree
Showing 8 changed files with 768 additions and 192 deletions.
2 changes: 1 addition & 1 deletion lib/client/api_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestTeleportClient_Login_localMFALogin(t *testing.T) {
promptWebauthn func(ctx context.Context, origin string, assertion *wanlib.CredentialAssertion) (*proto.MFAAuthenticateResponse, error)
}{}
var loginMocksMU sync.RWMutex
*client.PromptOTP = func(ctx context.Context, out io.Writer, in *prompt.ContextReader, question string) (string, error) {
*client.PromptOTP = func(ctx context.Context, out io.Writer, in prompt.Reader, question string) (string, error) {
loginMocksMU.RLock()
defer loginMocksMU.RUnlock()
return loginMocks.promptOTP(ctx)
Expand Down
42 changes: 35 additions & 7 deletions lib/utils/prompt/confirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,27 @@ import (
"github.com/gravitational/trace"
)

// Reader is the interface for prompt readers.
type Reader interface {
// ReadContext reads from the underlying buffer, respecting context
// cancellation.
ReadContext(ctx context.Context) ([]byte, error)
}

// SecureReader is the interface for password readers.
type SecureReader interface {
// ReadPassword reads from the underlying buffer, respecting context
// cancellation.
ReadPassword(ctx context.Context) ([]byte, error)
}

// Confirmation prompts the user for a yes/no confirmation for question.
// The prompt is written to out and the answer is read from in.
//
// question should be a plain sentece without "[yes/no]"-type hints at the end.
// question should be a plain sentence without "[yes/no]"-type hints at the end.
//
// ctx can be canceled to abort the prompt.
func Confirmation(ctx context.Context, out io.Writer, in *ContextReader, question string) (bool, error) {
func Confirmation(ctx context.Context, out io.Writer, in Reader, question string) (bool, error) {
fmt.Fprintf(out, "%s [y/N]: ", question)
answer, err := in.ReadContext(ctx)
if err != nil {
Expand All @@ -49,14 +63,14 @@ func Confirmation(ctx context.Context, out io.Writer, in *ContextReader, questio
// PickOne prompts the user to pick one of the provided string options.
// The prompt is written to out and the answer is read from in.
//
// question should be a plain sentece without the list of provided options.
// question should be a plain sentence without the list of provided options.
//
// ctx can be canceled to abort the prompt.
func PickOne(ctx context.Context, out io.Writer, in *ContextReader, question string, options []string) (string, error) {
func PickOne(ctx context.Context, out io.Writer, in Reader, question string, options []string) (string, error) {
fmt.Fprintf(out, "%s [%s]: ", question, strings.Join(options, ", "))
answerOrig, err := in.ReadContext(ctx)
if err != nil {
return "", trace.WrapWithMessage(err, "failed reading prompt response")
return "", trace.Wrap(err, "failed reading prompt response")
}
answer := strings.ToLower(strings.TrimSpace(string(answerOrig)))
for _, opt := range options {
Expand All @@ -72,11 +86,25 @@ func PickOne(ctx context.Context, out io.Writer, in *ContextReader, question str
// The prompt is written to out and the answer is read from in.
//
// ctx can be canceled to abort the prompt.
func Input(ctx context.Context, out io.Writer, in *ContextReader, question string) (string, error) {
func Input(ctx context.Context, out io.Writer, in Reader, question string) (string, error) {
fmt.Fprintf(out, "%s: ", question)
answer, err := in.ReadContext(ctx)
if err != nil {
return "", trace.WrapWithMessage(err, "failed reading prompt response")
return "", trace.Wrap(err, "failed reading prompt response")
}
return strings.TrimSpace(string(answer)), nil
}

// Password prompts the user for a password. The prompt is written to out and
// the answer is read from in.
// The in reader has to be a terminal.
func Password(ctx context.Context, out io.Writer, in SecureReader, question string) (string, error) {
if question != "" {
fmt.Fprintf(out, "%s:\n", question)
}
answer, err := in.ReadPassword(ctx)
if err != nil {
return "", trace.Wrap(err, "failed reading prompt response")
}
return string(answer), nil // passwords not trimmed
}
Loading

0 comments on commit 89f48f7

Please sign in to comment.