From abd3170e376248ccc758b7205e745e2ed8f8d8ad 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 | 56 ++++++++++++++++++++++++++++++++++---- 4 files changed, 104 insertions(+), 6 deletions(-) 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..404e9ca8 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{255, 255, 255, 255}) >= 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..d469f089 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" @@ -499,7 +500,9 @@ var _ = Describe("SSH Command", func() { var _ = Describe("SSH Options", func() { var ( - streams util.IOStreams + streams util.IOStreams + // out *util.SafeBytesBuffer + // in *util.SafeBytesBuffer publicSSHKeyFile string ) @@ -523,7 +526,7 @@ var _ = Describe("SSH Options", func() { It("should validate", func() { o := ssh.NewSSHOptions(streams) - o.CIDRs = []string{"8.8.8.8/32"} + o.CIDRs = []string{"8.8.8.8/24"} o.SSHPublicKeyFile = publicSSHKeyFile Expect(o.Validate()).To(Succeed()) @@ -531,7 +534,7 @@ var _ = Describe("SSH Options", func() { It("should require a non-zero wait time", func() { o := ssh.NewSSHOptions(streams) - o.CIDRs = []string{"8.8.8.8/32"} + o.CIDRs = []string{"8.8.8.8/24"} o.SSHPublicKeyFile = publicSSHKeyFile o.WaitTimeout = 0 @@ -540,7 +543,7 @@ var _ = Describe("SSH Options", func() { It("should require a public SSH key file", func() { o := ssh.NewSSHOptions(streams) - o.CIDRs = []string{"8.8.8.8/32"} + o.CIDRs = []string{"8.8.8.8/24"} Expect(o.Validate()).NotTo(Succeed()) }) @@ -549,7 +552,7 @@ var _ = Describe("SSH Options", func() { Expect(os.WriteFile(publicSSHKeyFile, []byte("not a key"), 0644)).To(Succeed()) o := ssh.NewSSHOptions(streams) - o.CIDRs = []string{"8.8.8.8/32"} + o.CIDRs = []string{"8.8.8.8/24"} o.SSHPublicKeyFile = publicSSHKeyFile Expect(o.Validate()).NotTo(Succeed()) @@ -569,4 +572,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 /32 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/32 without force flag", "8.8.8.8/32", 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/32 with force flag", "8.8.8.8/32", true, false), + ) })