1- // Package sudo provides utilities for checking and gating on sudo access.
21package sudo
32
43import (
@@ -21,27 +20,20 @@ const (
2120 StatusUncached Status = 2
2221)
2322
24- // Check returns the current sudo status.
25- func Check () Status {
26- if os .Getuid () == 0 {
27- return StatusRoot
28- }
29- if exec .Command ("sudo" , "-n" , "true" ).Run () == nil { //nolint:gosec // intentional sudo probe
30- return StatusCached
31- }
32- return StatusUncached
23+ // Gater gates execution on sudo availability; callers can use Default or inject a stub in tests.
24+ type Gater interface {
25+ Gate (t * terminal.Terminal , confirmer terminal.Confirmer , reason string , assumeYes bool ) error
3326}
3427
35- // Gate checks sudo status, informs the user, and asks for confirmation before
36- // proceeding. Returns nil if the user confirms (or is already root). Returns
37- // an error if the user declines. When assumeYes is true, the confirmation prompt is skipped.
38- func Gate (t * terminal.Terminal , confirmer terminal.Confirmer , reason string , assumeYes bool ) error {
39- status := Check ()
40- if status == StatusRoot {
41- return nil
42- }
28+ // Default is the Gater used by commands. Tests may replace it with a stub.
29+ var Default Gater = & systemGater {}
4330
44- if assumeYes {
31+ // systemGater implements Gater using the real sudo check and optional password prompt.
32+ type systemGater struct {}
33+
34+ func (g * systemGater ) Gate (t * terminal.Terminal , confirmer terminal.Confirmer , reason string , assumeYes bool ) error {
35+ status := check ()
36+ if status == StatusRoot {
4537 return nil
4638 }
4739
@@ -54,9 +46,52 @@ func Gate(t *terminal.Terminal, confirmer terminal.Confirmer, reason string, ass
5446 }
5547 t .Vprint ("" )
5648
57- if ! confirmer .ConfirmYesNo (fmt .Sprintf ("%s requires sudo. Continue?" , reason )) {
49+ if ! assumeYes && ! confirmer .ConfirmYesNo (fmt .Sprintf ("%s requires sudo. Continue?" , reason )) {
5850 return fmt .Errorf ("%s canceled by user" , reason )
5951 }
6052
53+ if status == StatusUncached {
54+ if exec .Command ("sudo" , "-n" , "-v" ).Run () != nil { //nolint:gosec // intentional sudo -n -v
55+ if isTTY (os .Stdin ) {
56+ cmd := exec .Command ("sudo" , "-v" ) //nolint:gosec // intentional sudo -v
57+ cmd .Stdin = os .Stdin
58+ cmd .Stdout = os .Stdout
59+ cmd .Stderr = os .Stderr
60+ if err := cmd .Run (); err != nil {
61+ return fmt .Errorf ("sudo authentication failed: %w" , err )
62+ }
63+ }
64+ }
65+ }
66+
6167 return nil
6268}
69+
70+ // check returns the current sudo status.
71+ func check () Status {
72+ if os .Getuid () == 0 {
73+ return StatusRoot
74+ }
75+ if exec .Command ("sudo" , "-n" , "true" ).Run () == nil { //nolint:gosec // intentional sudo probe
76+ return StatusCached
77+ }
78+ return StatusUncached
79+ }
80+
81+ func isTTY (f * os.File ) bool {
82+ if f == nil {
83+ return false
84+ }
85+ info , err := f .Stat ()
86+ if err != nil {
87+ return false
88+ }
89+ return (info .Mode () & os .ModeCharDevice ) != 0
90+ }
91+
92+
93+ // CachedGater is a Gater that always acts as if sudo is cached (no prompt, no refresh). For tests.
94+ type CachedGater struct {}
95+
96+ func (CachedGater ) Gate (* terminal.Terminal , terminal.Confirmer , string , bool ) error { return nil }
97+
0 commit comments