diff --git a/commands/stop.go b/commands/stop.go index 39807d7..31fde50 100644 --- a/commands/stop.go +++ b/commands/stop.go @@ -3,7 +3,6 @@ package commands import ( "fmt" - "github.com/fatih/color" "github.com/phase2/rig/util" "github.com/urfave/cli" ) @@ -58,15 +57,15 @@ func (cmd *Stop) StopOutrigger() error { } cmd.out.Info("Stopped machine '%s'", cmd.machine.Name) - cmd.out.Spin("Cleaning up local networking (may require your admin password)") + cmd.out.Spin("Cleaning up local networking...") if util.IsWindows() { util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.0.0").Run() util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.42.1").Run() } else { + util.EscalatePrivilege() util.Command("sudo", "route", "-n", "delete", "-net", "172.17.0.0").Run() util.Command("sudo", "route", "-n", "delete", "-net", "172.17.42.1").Run() } - color.Unset() cmd.out.Info("Networking cleanup completed") return cmd.Success(fmt.Sprintf("Machine '%s' stopped", cmd.machine.Name)) diff --git a/util/logger.go b/util/logger.go index 0095225..0fa30fb 100644 --- a/util/logger.go +++ b/util/logger.go @@ -1,11 +1,11 @@ package util import ( + "fmt" "io/ioutil" "log" "os" - "fmt" "github.com/fatih/color" spun "github.com/slok/gospinner" ) @@ -24,10 +24,11 @@ type logChannels struct { // RigLogger is the global logger object type RigLogger struct { - Channel logChannels - Progress *RigSpinner - IsVerbose bool - Spinning bool + Channel logChannels + Progress *RigSpinner + IsVerbose bool + Spinning bool + Privileged bool } // RigSpinner object wrapper to facilitate our spinner service @@ -51,9 +52,10 @@ func LoggerInit(verbose bool) { Error: log.New(os.Stderr, color.RedString("[ERROR] "), 0), Verbose: log.New(verboseWriter, "[VERBOSE] ", 0), }, - IsVerbose: verbose, - Progress: &RigSpinner{s}, - Spinning: false, + IsVerbose: verbose, + Progress: &RigSpinner{s}, + Spinning: false, + Privileged: false, } } @@ -125,3 +127,24 @@ func (log *RigLogger) Verbose(format string, a ...interface{}) { func (log *RigLogger) Note(format string, a ...interface{}) { log.Channel.Info.Println(fmt.Sprintf(format, a...)) } + +// PrivilegeEscallationPrompt interrupts a running spinner to ensure clear +// prompting to the user for sudo password entry. It is up to the caller to know +// that privilege is needed. This prompt is only displayed on the first privilege +// escallation of a given rig process. +func (log *RigLogger) PrivilegeEscallationPrompt() { + defer func() { log.Privileged = true }() + + if log.Privileged { + return + } + + // This newline ensures the last status before escallation is preserved + // on-screen. It creates extraneous space in verbose mode. + if !log.IsVerbose { + fmt.Println() + } + message := "Administrative privileges needed..." + log.Spin(message) + log.Warning(message) +} diff --git a/util/shell_exec.go b/util/shell_exec.go index 45bf785..2318ad5 100644 --- a/util/shell_exec.go +++ b/util/shell_exec.go @@ -39,6 +39,13 @@ func Convert(cmd *exec.Cmd) Executor { return Executor{cmd} } +// EscalatePrivilege attempts to gain administrative privilege +// @todo identify administrative escallation on Windows. +// E.g., "runas", "/noprofile", "/user:Administrator +func EscalatePrivilege() error { + return Command("sudo", "-v").Run() +} + // PassthruCommand is similar to ForceStreamCommand in that it will issue all output // regardless of verbose mode. Further, this version of the command captures the // exit status of any executed command. This function is intended to simulate @@ -92,36 +99,53 @@ func (x Executor) Execute(forceOutput bool) error { // CombinedOutput runs a command via exec.CombinedOutput() without modification or output of the underlying command. func (x Executor) CombinedOutput() ([]byte, error) { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.CombinedOutput() } // Run runs a command via exec.Run() without modification or output of the underlying command. func (x Executor) Run() error { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Run() } // Output runs a command via exec.Output() without modification or output of the underlying command. func (x Executor) Output() ([]byte, error) { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Output() } // Start runs a command via exec.Start() without modification or output of the underlying command. func (x Executor) Start() error { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Start() } // Log verbosely logs the command. func (x Executor) Log(tag string) { color.Set(color.FgMagenta) - Logger().Verbose("%s: %s", tag, x.ToString()) + Logger().Verbose("%s: %s", tag, x) color.Unset() } -// ToString converts a Command to a human-readable string with key context details. -func (x Executor) ToString() string { +// String converts a Command to a human-readable string with key context details. +// It is automatically applied in contexts such as fmt functions. +func (x Executor) String() string { context := "" if x.cmd.Dir != "" { context = fmt.Sprintf("(WD: %s", x.cmd.Dir) @@ -137,3 +161,12 @@ func (x Executor) ToString() string { return fmt.Sprintf("%s %s %s", x.cmd.Path, strings.Join(x.cmd.Args[1:], " "), context) } + +// IsPrivileged evaluates the command to determine if administrative privilege +// is required. +// @todo identify administrative escallation on Windows. +// E.g., "runas", "/noprofile", "/user:Administrator +func (x Executor) IsPrivileged() bool { + _, privileged := IndexOfSubstring(x.cmd.Args, "sudo") + return privileged +} diff --git a/util/slices.go b/util/slices.go new file mode 100644 index 0000000..30aa68d --- /dev/null +++ b/util/slices.go @@ -0,0 +1,30 @@ +package util + +import ( + "strings" +) + +// IndexOfString is a general utility function that can find the index of a value +// present in a string slice. The second value is true if the item is found. +func IndexOfString(slice []string, search string) (int, bool) { + for index, elem := range slice { + if elem == search { + return index, true + } + } + + return 0, false +} + +// IndexOfSubstring is a variation on IndexOfString which checks to see if a +// given slice value matches our search string, or if that search string is +// a substring of the element. The second value is true if the item is found. +func IndexOfSubstring(slice []string, search string) (int, bool) { + for index, elem := range slice { + if strings.Contains(elem, search) { + return index, true + } + } + + return 0, false +}