Skip to content

Commit

Permalink
feat(gardener#161): adds a prompt when providing /32 CIDR ranges
Browse files Browse the repository at this point in the history
including an --force flag that hides such prompts for non-interactive/script usage
  • Loading branch information
sven-petersen committed Oct 26, 2022
1 parent 445e728 commit abd3170
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 6 deletions.
8 changes: 8 additions & 0 deletions pkg/cmd/ssh/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"context"
"os"
"time"

"github.com/gardener/gardenctl-v2/internal/util"
)

func SetBastionAvailabilityChecker(f func(hostname string, privateKey []byte) error) {
Expand Down Expand Up @@ -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
}
45 changes: 44 additions & 1 deletion pkg/cmd/ssh/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ SPDX-License-Identifier: Apache-2.0
package ssh

import (
"bufio"
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
56 changes: 51 additions & 5 deletions pkg/cmd/ssh/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
)

Expand All @@ -523,15 +526,15 @@ 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())
})

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

Expand All @@ -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())
})
Expand All @@ -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())
Expand All @@ -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),
)
})

0 comments on commit abd3170

Please sign in to comment.