From 05c988b87708c037ee8c4139a4e5ae1998c56cc7 Mon Sep 17 00:00:00 2001 From: Sven Petersen <5971734+sven-petersen@users.noreply.github.com> Date: Tue, 25 Oct 2022 16:43:24 +0200 Subject: [PATCH] feat(#161): adds a prompt when providing /32 CIDR ranges including an --force flag that hides such prompts for non-interactive/script usage --- pkg/cmd/ssh/export_test.go | 8 +++++++ pkg/cmd/ssh/options.go | 45 +++++++++++++++++++++++++++++++++++++- pkg/cmd/ssh/ssh.go | 1 + pkg/cmd/ssh/ssh_test.go | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/ssh/export_test.go b/pkg/cmd/ssh/export_test.go index 9f7f8872..b3ce3439 100644 --- a/pkg/cmd/ssh/export_test.go +++ b/pkg/cmd/ssh/export_test.go @@ -10,6 +10,8 @@ import ( "context" "os" "time" + + "github.com/gardener/gardenctl-v2/internal/util" ) func SetBastionAvailabilityChecker(f func(hostname string, privateKey []byte) error) { @@ -42,3 +44,9 @@ func SetKeepAliveInterval(d time.Duration) { keepAliveInterval = d } + +var Confirm = confirm + +func SetConfirm(f func(ioStreams util.IOStreams, question string, defaultAnswer bool) bool) { + confirm = f +} diff --git a/pkg/cmd/ssh/options.go b/pkg/cmd/ssh/options.go index bab1fbbb..6bfdc6a7 100644 --- a/pkg/cmd/ssh/options.go +++ b/pkg/cmd/ssh/options.go @@ -7,6 +7,8 @@ SPDX-License-Identifier: Apache-2.0 package ssh import ( + "bufio" + "bytes" "context" "crypto/rand" "crypto/rsa" @@ -151,6 +153,34 @@ var ( return cmd.Run() } + + // Displays a prompt to the user to confirm (yes/no) something + confirm = func(ioStreams util.IOStreams, question string, defaultAnswer bool) bool { + reader := bufio.NewReader(ioStreams.In) + + choices := "y/N" + if defaultAnswer { + choices = "n/Y" + } + + for { + fmt.Fprint(ioStreams.Out, question+" ["+choices+"]: ") + + str, _ := reader.ReadString('\n') + + str = strings.TrimSpace(str) + str = strings.ToLower(str) + + switch str { + case "": + return defaultAnswer + case "n", "no": + return false + case "y", "yes": + return true + } + } + } ) // SSHOptions is a struct to support ssh command @@ -199,6 +229,10 @@ type SSHOptions struct { // bastion once it exits. By default it deletes it, but we allow the user to // keep it for debugging purposes. KeepBastion bool + + // Force will silence warnings and interactive prompts. The latter happens if the user + // specifies a /32/large/ CIDR range which usually requires the users confirmation. + Force bool } // NewSSHOptions returns initialized SSHOptions @@ -301,9 +335,18 @@ func (o *SSHOptions) Validate() error { } for _, cidr := range o.CIDRs { - if _, _, err := net.ParseCIDR(cidr); err != nil { + _, netIP, err := net.ParseCIDR(cidr) + if err != nil { return fmt.Errorf("CIDR %q is invalid: %w", cidr, err) } + + if !o.Force && bytes.Compare(netIP.Mask, []byte{0, 0, 0, 0}) <= 0 { + question := fmt.Sprintf("Large CIDR range %s compromises security. Continue?", cidr) + if !confirm(o.IOStreams, question, false) { + // return fmt.Errorf("") + os.Exit(0) + } + } } content, err := os.ReadFile(o.SSHPublicKeyFile) diff --git a/pkg/cmd/ssh/ssh.go b/pkg/cmd/ssh/ssh.go index 7a60bf18..3e949e8b 100644 --- a/pkg/cmd/ssh/ssh.go +++ b/pkg/cmd/ssh/ssh.go @@ -42,6 +42,7 @@ func NewCmdSSH(f util.Factory, o *SSHOptions) *cobra.Command { cmd.Flags().StringVar(&o.SSHPublicKeyFile, "public-key-file", "", "Path to the file that contains a public SSH key. If not given, a temporary keypair will be generated.") cmd.Flags().DurationVar(&o.WaitTimeout, "wait-timeout", o.WaitTimeout, "Maximum duration to wait for the bastion to become available.") cmd.Flags().BoolVar(&o.KeepBastion, "keep-bastion", o.KeepBastion, "Do not delete immediately when gardenctl exits (Bastions will be garbage-collected after some time)") + cmd.Flags().BoolVar(&o.Force, "force", o.Force, "Do not show warnings and do not prompt for confirmation. Does not affect access control warnings.") return cmd } diff --git a/pkg/cmd/ssh/ssh_test.go b/pkg/cmd/ssh/ssh_test.go index bc74eafe..4a95e748 100644 --- a/pkg/cmd/ssh/ssh_test.go +++ b/pkg/cmd/ssh/ssh_test.go @@ -20,6 +20,7 @@ import ( "github.com/gardener/gardener/pkg/utils/secrets" "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -569,4 +570,47 @@ var _ = Describe("SSH Options", func() { Expect(o.Validate()).NotTo(Succeed()) }) + + DescribeTable("Prompt should print question and should return...", func(defaultAnswer bool, answer string, expectedResult bool) { + question := "Is 42 the ultimate answer?" + expectedPrompt := question + if defaultAnswer { + expectedPrompt += " [n/Y]: " + } else { + expectedPrompt += " [y/N]: " + } + streams, stdin, stdout, _ := util.NewTestIOStreams() + + stdin.Write([]byte(answer)) + result := ssh.Confirm(streams, question, defaultAnswer) + stdoutStr := stdout.String() + + Expect(stdoutStr).To(Equal(expectedPrompt)) + Expect(result).To(Equal(expectedResult)) + }, + Entry("true (defaultAnswer: false, given answer: y )", false, "y", true), + Entry("false (defaultAnswer: false, given answer: n )", false, "n", false), + Entry("false (defaultAnswer: false, given answer: \\n)", false, "\n", false), + Entry("true (defaultAnswer: true , given answer: yEs )", true, "yEs", true), + Entry("false (defaultAnswer: true , given answer: nO )", true, "nO", false), + Entry("true (defaultAnswer: true , given answer: \\n)", true, "\n", true), + ) + + DescribeTable("Should prompt for confirmation in case of /0 CIDRs without force-flag", func(cidr string, forceFlag, shouldConfirm bool) { + o := ssh.NewSSHOptions(streams) + o.CIDRs = []string{cidr} + o.SSHPublicKeyFile = publicSSHKeyFile + o.Force = forceFlag + confirmCalled := false + ssh.SetConfirm(func(ioStreams util.IOStreams, question string, defaultAnswer bool) bool { + confirmCalled = true + return true + }) + o.Validate() + Expect(confirmCalled).To(Equal(shouldConfirm)) + }, + Entry("testing CIDR 8.8.8.8/0 without force flag", "8.8.8.8/0", false, true), + Entry("testing CIDR 8.8.8.8/16 without force flag", "8.8.8.8/16", false, false), + Entry("testing CIDR 8.8.8.8/0 with force flag", "8.8.8.8/0", true, false), + ) })