Skip to content

Commit

Permalink
Initial support for enabling prompts to react to --force flag.
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchell-as committed Nov 18, 2024
1 parent b97540c commit b5451fc
Show file tree
Hide file tree
Showing 27 changed files with 257 additions and 153 deletions.
14 changes: 12 additions & 2 deletions cmd/state/internal/cmdtree/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ func newCleanUninstallCommand(prime *primer.Values, globals *globalOptions) *cap
return err
}

params.NonInteractive = globals.NonInteractive // distinct from --force
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
if params.Force {
prime.Prompt().EnableForce()
}
return runner.Run(&params)
},
)
Expand All @@ -83,7 +88,9 @@ func newCleanCacheCommand(prime *primer.Values, globals *globalOptions) *captain
},
},
func(ccmd *captain.Command, _ []string) error {
params.Force = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return runner.Run(&params)
},
)
Expand All @@ -107,6 +114,9 @@ func newCleanConfigCommand(prime *primer.Values) *captain.Command {
},
[]*captain.Argument{},
func(ccmd *captain.Command, _ []string) error {
if params.Force {
prime.Prompt().EnableForce()
}
return runner.Run(&params)
},
)
Expand Down
9 changes: 9 additions & 0 deletions cmd/state/internal/cmdtree/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func newInstallCommand(prime *primer.Values) *captain.Command {
runner := install.New(prime, model.NamespacePackage)

params := install.Params{}
force := false

var packagesRaw string
cmd := captain.NewCommand(
Expand All @@ -67,6 +68,11 @@ func newInstallCommand(prime *primer.Values) *captain.Command {
Description: locale.T("package_flag_ts_description"),
Value: &params.Timestamp,
},
{
Name: "force",
Description: locale.Tl("package_flag_force_description", "Ignore security policy preventing packages with CVEs from being installed (not recommended)"),
Value: &force,
},
},
[]*captain.Argument{
{
Expand All @@ -82,6 +88,9 @@ func newInstallCommand(prime *primer.Values) *captain.Command {
return locale.WrapInputError(err, "err_install_packages_args", "Invalid install arguments")
}
}
if force {
prime.Prompt().EnableForce()
}
return runner.Run(params)
},
)
Expand Down
4 changes: 3 additions & 1 deletion cmd/state/internal/cmdtree/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ func newResetCommand(prime *primer.Values, globals *globalOptions) *captain.Comm
},
},
func(ccmd *captain.Command, args []string) error {
params.Force = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return runner.Run(params)
},
).SetGroup(VCSGroup).SetSupportsStructuredOutput()
Expand Down
4 changes: 3 additions & 1 deletion cmd/state/internal/cmdtree/revert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func newRevertCommand(prime *primer.Values, globals *globalOptions) *captain.Com
},
},
func(ccmd *captain.Command, args []string) error {
params.Force = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return runner.Run(params)
},
).SetGroup(VCSGroup).SetSupportsStructuredOutput()
Expand Down
8 changes: 6 additions & 2 deletions cmd/state/internal/cmdtree/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ func newUpdateLockCommand(prime *primer.Values, globals *globalOptions) *captain
},
[]*captain.Argument{},
func(cmd *captain.Command, args []string) error {
params.NonInteractive = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return runner.Run(&params)
},
)
Expand All @@ -73,7 +75,9 @@ func newUpdateUnlockCommand(prime *primer.Values, globals *globalOptions) *capta
[]*captain.Flag{},
[]*captain.Argument{},
func(cmd *captain.Command, args []string) error {
params.NonInteractive = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return runner.Run(&params)
},
)
Expand Down
4 changes: 3 additions & 1 deletion cmd/state/internal/cmdtree/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func newUseResetCommand(prime *primer.Values, globals *globalOptions) *captain.C
[]*captain.Flag{},
[]*captain.Argument{},
func(_ *captain.Command, _ []string) error {
params.Force = globals.NonInteractive
if globals.NonInteractive {
prime.Prompt().SetInteractive(false)
}
return use.NewReset(prime).Run(params)
},
)
Expand Down
6 changes: 6 additions & 0 deletions internal/locale/locales/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,12 @@ warning_vulnerable_short:
prompt_continue_pkg_operation:
other: |
Do you want to continue installing this dependency despite its vulnerabilities?
prompt_continue_force:
other: "Continuing because the '[ACTIONABLE]--force[/RESET]' flag is set."
prompt_continue_non_interactive:
other: "Continuting because State Tool is running in non-interactive mode."
prompt_abort_non_interactive:
other: "Aborting because State Tool is running in non-interactive mode. To bypass you can use the '[ACTIONABLE]--force[/RESET]' flag."
unstable_command_warning:
other: |
This command is still in beta. If you want to opt-in to unstable features, run the following command:
Expand Down
49 changes: 41 additions & 8 deletions internal/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,37 @@ import (
"gopkg.in/AlecAivazis/survey.v1/terminal"

"github.com/ActiveState/cli/internal/analytics/constants"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/output"
)

type ConfirmKind int

const (
// A confirm prompt was completed by the user.
User ConfirmKind = iota
// A confirm prompt was completed in non-interactive mode.
NonInteractive
// A confirm prompt was completed via --force.
Force
)

type EventDispatcher interface {
EventWithLabel(category, action string, label string, dim ...*dimensions.Values)
}

// Prompter is the interface used to run our prompt from, useful for mocking in tests
// Prompter is the interface used to run our prompt from
type Prompter interface {
Input(title, message string, defaultResponse *string, flags ...ValidatorFlag) (string, error)
InputAndValidate(title, message string, defaultResponse *string, validator ValidatorFunc, flags ...ValidatorFlag) (string, error)
Select(title, message string, choices []string, defaultResponse *string) (string, error)
Confirm(title, message string, defaultChoice *bool) (bool, error)
Confirm(title, message string, defaultChoice *bool, forcedChoice *bool) (bool, ConfirmKind, error)
InputSecret(title, message string, flags ...ValidatorFlag) (string, error)
IsInteractive() bool
SetInteractive(bool)
EnableForce()
}

// ValidatorFunc is a function pass to the Prompter to perform validation
Expand All @@ -39,18 +53,29 @@ type Prompt struct {
out output.Outputer
analytics EventDispatcher
isInteractive bool
isForced bool
}

// New creates a new prompter
func New(isInteractive bool, an EventDispatcher) Prompter {
return &Prompt{output.Get(), an, isInteractive}
return &Prompt{output.Get(), an, isInteractive, false}
}

// IsInteractive checks if the prompts can be interactive or should just return default values
func (p *Prompt) IsInteractive() bool {
return p.isInteractive
}

func (p *Prompt) SetInteractive(interactive bool) {
p.isInteractive = interactive
}

// EnableForce forces confirm prompts to return the force value (which is often different from the
// non-interactive value).
func (p *Prompt) EnableForce() {
p.isForced = true
}

// ValidatorFlag represents flags for prompt functions to change their behavior on.
type ValidatorFlag int

Expand Down Expand Up @@ -158,13 +183,21 @@ func (p *Prompt) Select(title, message string, choices []string, defaultChoice *
}

// Confirm prompts user for yes or no response.
func (p *Prompt) Confirm(title, message string, defaultChoice *bool) (bool, error) {
func (p *Prompt) Confirm(title, message string, defaultChoice *bool, forcedChoice *bool) (bool, ConfirmKind, error) {
if p.isForced {
if forcedChoice == nil {
return false, Force, errs.New("No force option given for force-enabled prompt")
}
logging.Debug("Prompt %s confirmed with choice %v in force mode", title, forcedChoice)
return *forcedChoice, Force, nil
}

if !p.isInteractive {
if defaultChoice != nil {
logging.Debug("Prompt %s confirmed with default choice %v in non-interactive mode", title, defaultChoice)
return *defaultChoice, nil
return *defaultChoice, NonInteractive, nil
}
return false, interactiveInputError(message)
return false, NonInteractive, interactiveInputError(message)
}
if title != "" {
p.out.Notice(output.Emphasize(title))
Expand All @@ -186,11 +219,11 @@ func (p *Prompt) Confirm(title, message string, defaultChoice *bool) (bool, erro
if err == terminal.InterruptErr {
p.analytics.EventWithLabel(constants.CatPrompt, title, "interrupt")
}
return false, locale.NewInputError(err.Error())
return false, User, locale.NewInputError(err.Error())
}
p.analytics.EventWithLabel(constants.CatPrompt, title, translateConfirm(resp))

return resp, nil
return resp, User, nil
}

func translateConfirm(confirm bool) string {
Expand Down
20 changes: 11 additions & 9 deletions internal/runbits/auth/keypair.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/prompt"
"github.com/ActiveState/cli/internal/rtutils/ptr"
secretsapi "github.com/ActiveState/cli/pkg/platform/api/secrets"
secretsModels "github.com/ActiveState/cli/pkg/platform/api/secrets/secrets_models"
"github.com/ActiveState/cli/pkg/platform/authentication"
Expand Down Expand Up @@ -112,19 +113,20 @@ func promptForPreviousPassphrase(prompt prompt.Prompter) (string, error) {
return passphrase, nil
}

func promptUserToRegenerateKeypair(passphrase string, cfg keypairs.Configurable, out output.Outputer, prompt prompt.Prompter, auth *authentication.Auth) error {
var err error
func promptUserToRegenerateKeypair(passphrase string, cfg keypairs.Configurable, out output.Outputer, prmpt prompt.Prompter, auth *authentication.Auth) error {
// previous passphrase is invalid, inform user and ask if they want to generate a new keypair
out.Notice(locale.T("auth_generate_new_keypair_message"))
yes, err := prompt.Confirm("", locale.T("auth_confirm_generate_new_keypair_prompt"), new(bool))
yes, kind, err := prmpt.Confirm("", locale.T("auth_confirm_generate_new_keypair_prompt"), ptr.To(false), nil)
if err != nil {
return err
return errs.Wrap(err, "Unable to confirm")
}
if yes {
_, err = keypairs.GenerateAndSaveEncodedKeypair(cfg, secretsapi.Get(auth), passphrase, constants.DefaultRSABitLength, auth)
// TODO delete user's secrets
} else {
err = locale.NewError("auth_err_unrecoverable_keypair")
if !yes {
if kind == prompt.NonInteractive {
return locale.NewInputError("prompt_abort_non_interactive")
}
return locale.NewInputError("auth_err_unrecoverable_keypair")
}
_, err = keypairs.GenerateAndSaveEncodedKeypair(cfg, secretsapi.Get(auth), passphrase, constants.DefaultRSABitLength, auth)
// TODO delete user's secrets
return err
}
2 changes: 1 addition & 1 deletion internal/runbits/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func authenticateWithBrowser(out output.Outputer, auth *authentication.Auth, pro
var cont bool
var err error
for !cont {
cont, err = prompt.Confirm(locale.Tl("continue", "Continue?"), locale.T("auth_press_enter"), ptr.To(false))
cont, _, err = prompt.Confirm(locale.Tl("continue", "Continue?"), locale.T("auth_press_enter"), ptr.To(false), nil)
if err != nil {
return errs.Wrap(err, "Prompt failed")
}
Expand Down
29 changes: 12 additions & 17 deletions internal/runbits/cves/cves.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
configMediator "github.com/ActiveState/cli/internal/mediators/config"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/internal/prompt"
"github.com/ActiveState/cli/internal/rtutils/ptr"
"github.com/ActiveState/cli/pkg/buildplan"
vulnModel "github.com/ActiveState/cli/pkg/platform/api/vulnerabilities/model"
Expand Down Expand Up @@ -104,20 +105,24 @@ func (c *CveReport) Report(newBuildPlan *buildplan.BuildPlan, oldBuildPlan *buil
}

c.summarizeCVEs(vulnerabilities)
cont, err := c.promptForSecurity()

confirm, kind, err := c.prime.Prompt().Confirm("", locale.Tr("prompt_continue_pkg_operation"), ptr.To(false), ptr.To(true))
if err != nil {
return errs.Wrap(err, "Failed to prompt for security")
return errs.Wrap(err, "Unable to confirm")
}

if !cont {
if !c.prime.Prompt().IsInteractive() {
if !confirm {
if kind == prompt.NonInteractive {
return errs.AddTips(
locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt"),
locale.NewInputError("prompt_abort_non_interactive"),
locale.Tl("more_info_prompt", "To disable security prompting run: [ACTIONABLE]state config set security.prompt.enabled false[/RESET]"),
)
}
return locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt")
return locale.NewInputError("err_pkgop_security_prompt", "Operation aborted by user")
}
if kind == prompt.Force {
c.prime.Output().Notice(locale.T("prompt_continue_force"))
}
c.prime.Output().Notice("") // Empty line

return nil
}
Expand Down Expand Up @@ -225,16 +230,6 @@ func (c *CveReport) summarizeCVEs(vulnerabilities model.VulnerableIngredientsByL
out.Print("")
}

func (c *CveReport) promptForSecurity() (bool, error) {
confirm, err := c.prime.Prompt().Confirm("", locale.Tr("prompt_continue_pkg_operation"), ptr.To(false))
if err != nil {
return false, locale.WrapError(err, "err_pkgop_confirm", "Need a confirmation.")
}
c.prime.Output().Notice("") // Empty line

return confirm, nil
}

func changedRequirements(oldBuildPlan *buildplan.BuildPlan, newBuildPlan *buildplan.BuildPlan) []string {
var names []string
var oldRequirements buildplan.Requirements
Expand Down
Loading

0 comments on commit b5451fc

Please sign in to comment.