Skip to content

Commit 09ff3c7

Browse files
authored
Release 2.1.2 (#143)
* Add explicit privilege prompt to improve sudo UX (#138) * Explicitly prompt for privilege escallation * Remove password prompt part of privilege message * Expand sudo detection. * Tidy up timing issues. * Consolidate messaging and avoid newline in verbose. * Cleanup ToString, sudo contains, cover more exec methods. * Lint does not catch all of fmt. * Remove unnecessary password prompt from networking cleanup. * Remove color reset and cat /dev/null to clear route text. * trying a different approach to requesting for admin privs (#144)
1 parent 85ba91b commit 09ff3c7

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

commands/stop.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package commands
33
import (
44
"fmt"
55

6-
"github.com/fatih/color"
76
"github.com/phase2/rig/util"
87
"github.com/urfave/cli"
98
)
@@ -58,15 +57,15 @@ func (cmd *Stop) StopOutrigger() error {
5857
}
5958
cmd.out.Info("Stopped machine '%s'", cmd.machine.Name)
6059

61-
cmd.out.Spin("Cleaning up local networking (may require your admin password)")
60+
cmd.out.Spin("Cleaning up local networking...")
6261
if util.IsWindows() {
6362
util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.0.0").Run()
6463
util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.42.1").Run()
6564
} else {
65+
util.EscalatePrivilege()
6666
util.Command("sudo", "route", "-n", "delete", "-net", "172.17.0.0").Run()
6767
util.Command("sudo", "route", "-n", "delete", "-net", "172.17.42.1").Run()
6868
}
69-
color.Unset()
7069
cmd.out.Info("Networking cleanup completed")
7170

7271
return cmd.Success(fmt.Sprintf("Machine '%s' stopped", cmd.machine.Name))

util/logger.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package util
22

33
import (
4+
"fmt"
45
"io/ioutil"
56
"log"
67
"os"
78

8-
"fmt"
99
"github.com/fatih/color"
1010
spun "github.com/slok/gospinner"
1111
)
@@ -24,10 +24,11 @@ type logChannels struct {
2424

2525
// RigLogger is the global logger object
2626
type RigLogger struct {
27-
Channel logChannels
28-
Progress *RigSpinner
29-
IsVerbose bool
30-
Spinning bool
27+
Channel logChannels
28+
Progress *RigSpinner
29+
IsVerbose bool
30+
Spinning bool
31+
Privileged bool
3132
}
3233

3334
// RigSpinner object wrapper to facilitate our spinner service
@@ -51,9 +52,10 @@ func LoggerInit(verbose bool) {
5152
Error: log.New(os.Stderr, color.RedString("[ERROR] "), 0),
5253
Verbose: log.New(verboseWriter, "[VERBOSE] ", 0),
5354
},
54-
IsVerbose: verbose,
55-
Progress: &RigSpinner{s},
56-
Spinning: false,
55+
IsVerbose: verbose,
56+
Progress: &RigSpinner{s},
57+
Spinning: false,
58+
Privileged: false,
5759
}
5860
}
5961

@@ -125,3 +127,24 @@ func (log *RigLogger) Verbose(format string, a ...interface{}) {
125127
func (log *RigLogger) Note(format string, a ...interface{}) {
126128
log.Channel.Info.Println(fmt.Sprintf(format, a...))
127129
}
130+
131+
// PrivilegeEscallationPrompt interrupts a running spinner to ensure clear
132+
// prompting to the user for sudo password entry. It is up to the caller to know
133+
// that privilege is needed. This prompt is only displayed on the first privilege
134+
// escallation of a given rig process.
135+
func (log *RigLogger) PrivilegeEscallationPrompt() {
136+
defer func() { log.Privileged = true }()
137+
138+
if log.Privileged {
139+
return
140+
}
141+
142+
// This newline ensures the last status before escallation is preserved
143+
// on-screen. It creates extraneous space in verbose mode.
144+
if !log.IsVerbose {
145+
fmt.Println()
146+
}
147+
message := "Administrative privileges needed..."
148+
log.Spin(message)
149+
log.Warning(message)
150+
}

util/shell_exec.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ func Convert(cmd *exec.Cmd) Executor {
3939
return Executor{cmd}
4040
}
4141

42+
// EscalatePrivilege attempts to gain administrative privilege
43+
// @todo identify administrative escallation on Windows.
44+
// E.g., "runas", "/noprofile", "/user:Administrator
45+
func EscalatePrivilege() error {
46+
return Command("sudo", "-v").Run()
47+
}
48+
4249
// PassthruCommand is similar to ForceStreamCommand in that it will issue all output
4350
// regardless of verbose mode. Further, this version of the command captures the
4451
// exit status of any executed command. This function is intended to simulate
@@ -92,36 +99,53 @@ func (x Executor) Execute(forceOutput bool) error {
9299
// CombinedOutput runs a command via exec.CombinedOutput() without modification or output of the underlying command.
93100
func (x Executor) CombinedOutput() ([]byte, error) {
94101
x.Log("Executing")
102+
if out := Logger(); out != nil && x.IsPrivileged() {
103+
out.PrivilegeEscallationPrompt()
104+
defer out.Spin("Resuming operation...")
105+
}
95106
return x.cmd.CombinedOutput()
96107
}
97108

98109
// Run runs a command via exec.Run() without modification or output of the underlying command.
99110
func (x Executor) Run() error {
100111
x.Log("Executing")
112+
if out := Logger(); out != nil && x.IsPrivileged() {
113+
out.PrivilegeEscallationPrompt()
114+
defer out.Spin("Resuming operation...")
115+
}
101116
return x.cmd.Run()
102117
}
103118

104119
// Output runs a command via exec.Output() without modification or output of the underlying command.
105120
func (x Executor) Output() ([]byte, error) {
106121
x.Log("Executing")
122+
if out := Logger(); out != nil && x.IsPrivileged() {
123+
out.PrivilegeEscallationPrompt()
124+
defer out.Spin("Resuming operation...")
125+
}
107126
return x.cmd.Output()
108127
}
109128

110129
// Start runs a command via exec.Start() without modification or output of the underlying command.
111130
func (x Executor) Start() error {
112131
x.Log("Executing")
132+
if out := Logger(); out != nil && x.IsPrivileged() {
133+
out.PrivilegeEscallationPrompt()
134+
defer out.Spin("Resuming operation...")
135+
}
113136
return x.cmd.Start()
114137
}
115138

116139
// Log verbosely logs the command.
117140
func (x Executor) Log(tag string) {
118141
color.Set(color.FgMagenta)
119-
Logger().Verbose("%s: %s", tag, x.ToString())
142+
Logger().Verbose("%s: %s", tag, x)
120143
color.Unset()
121144
}
122145

123-
// ToString converts a Command to a human-readable string with key context details.
124-
func (x Executor) ToString() string {
146+
// String converts a Command to a human-readable string with key context details.
147+
// It is automatically applied in contexts such as fmt functions.
148+
func (x Executor) String() string {
125149
context := ""
126150
if x.cmd.Dir != "" {
127151
context = fmt.Sprintf("(WD: %s", x.cmd.Dir)
@@ -137,3 +161,12 @@ func (x Executor) ToString() string {
137161

138162
return fmt.Sprintf("%s %s %s", x.cmd.Path, strings.Join(x.cmd.Args[1:], " "), context)
139163
}
164+
165+
// IsPrivileged evaluates the command to determine if administrative privilege
166+
// is required.
167+
// @todo identify administrative escallation on Windows.
168+
// E.g., "runas", "/noprofile", "/user:Administrator
169+
func (x Executor) IsPrivileged() bool {
170+
_, privileged := IndexOfSubstring(x.cmd.Args, "sudo")
171+
return privileged
172+
}

util/slices.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package util
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// IndexOfString is a general utility function that can find the index of a value
8+
// present in a string slice. The second value is true if the item is found.
9+
func IndexOfString(slice []string, search string) (int, bool) {
10+
for index, elem := range slice {
11+
if elem == search {
12+
return index, true
13+
}
14+
}
15+
16+
return 0, false
17+
}
18+
19+
// IndexOfSubstring is a variation on IndexOfString which checks to see if a
20+
// given slice value matches our search string, or if that search string is
21+
// a substring of the element. The second value is true if the item is found.
22+
func IndexOfSubstring(slice []string, search string) (int, bool) {
23+
for index, elem := range slice {
24+
if strings.Contains(elem, search) {
25+
return index, true
26+
}
27+
}
28+
29+
return 0, false
30+
}

0 commit comments

Comments
 (0)