cmd/drover: UAC re-launch helper for non-admin invocations
CLI subcommands (check/version/update) don't need driver access and
run as user. Bare drover.exe (GUI/engine mode) requires admin for
WinDivertOpen — re-launches via ShellExecute("runas") and exits.
Per spec decision B1: prompt at every launch, no scheduled-task
trampoline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,17 @@ func main() {
|
|||||||
// AttachConsole(ATTACH_PARENT_PROCESS) wires that up. No-op elsewhere.
|
// AttachConsole(ATTACH_PARENT_PROCESS) wires that up. No-op elsewhere.
|
||||||
attachToParentConsole()
|
attachToParentConsole()
|
||||||
|
|
||||||
|
// Detect if we need admin for the command in os.Args[1:]. If we do and
|
||||||
|
// we're not admin, re-launch via ShellExecute("runas", ...) and exit.
|
||||||
|
// CLI subcommands like "check", "version", "update" don't need admin
|
||||||
|
// and will run without UAC prompt.
|
||||||
|
if CmdNeedsAdmin(os.Args[1:]) && !IsAdmin() {
|
||||||
|
if err := ReElevate(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to re-elevate: %v\n", err)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Inject our build version so the updater package can stamp it on the
|
// Inject our build version so the updater package can stamp it on the
|
||||||
// User-Agent header it sends to git.okcu.io.
|
// User-Agent header it sends to git.okcu.io.
|
||||||
updater.SetVersion(Version)
|
updater.SetVersion(Version)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsAdmin returns true when the current process token has elevation.
|
||||||
|
// Wraps GetTokenInformation(TokenElevation).
|
||||||
|
func IsAdmin() bool {
|
||||||
|
var token windows.Token
|
||||||
|
if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer token.Close()
|
||||||
|
|
||||||
|
var elevation uint32
|
||||||
|
var sz uint32
|
||||||
|
err := windows.GetTokenInformation(
|
||||||
|
token,
|
||||||
|
windows.TokenElevation,
|
||||||
|
(*byte)(unsafe.Pointer(&elevation)),
|
||||||
|
uint32(unsafe.Sizeof(elevation)),
|
||||||
|
&sz,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return elevation != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdNeedsAdmin reports whether the given CLI args land in a code path
|
||||||
|
// that requires a WinDivert handle (and therefore admin). The default
|
||||||
|
// (no args = GUI mode) needs admin; explicit subcommands like check,
|
||||||
|
// version, update do not.
|
||||||
|
func CmdNeedsAdmin(args []string) bool {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return true // bare drover.exe → GUI/engine
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "check", "version", "--version", "-v", "update", "--help", "-h", "help":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReElevate re-launches the current executable with the given args via
|
||||||
|
// ShellExecuteW("runas", ...). On success the caller should os.Exit(0)
|
||||||
|
// immediately. Returns nil even when the user cancels UAC — the caller
|
||||||
|
// can't distinguish; we just exit cleanly afterward.
|
||||||
|
func ReElevate(args []string) error {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verb, _ := syscall.UTF16PtrFromString("runas")
|
||||||
|
exePtr, _ := syscall.UTF16PtrFromString(exe)
|
||||||
|
|
||||||
|
var paramsPtr *uint16
|
||||||
|
if len(args) > 0 {
|
||||||
|
// Quote each arg in case of spaces.
|
||||||
|
quoted := make([]string, len(args))
|
||||||
|
for i, a := range args {
|
||||||
|
quoted[i] = `"` + a + `"`
|
||||||
|
}
|
||||||
|
joined := ""
|
||||||
|
for i, q := range quoted {
|
||||||
|
if i > 0 {
|
||||||
|
joined += " "
|
||||||
|
}
|
||||||
|
joined += q
|
||||||
|
}
|
||||||
|
paramsPtr, _ = syscall.UTF16PtrFromString(joined)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
|
||||||
|
|
||||||
|
// SW_NORMAL = 1
|
||||||
|
return windows.ShellExecute(0, verb, exePtr, paramsPtr, cwdPtr, 1)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsAdmin_Smoke(t *testing.T) {
|
||||||
|
// Smoke test: IsAdmin returns a bool without panicking.
|
||||||
|
// We can't assert true/false without knowing the test environment,
|
||||||
|
// but we ensure the syscall path doesn't crash.
|
||||||
|
_ = IsAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdNeedsAdmin_NoAdminFlags(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
args []string
|
||||||
|
needsAdm bool
|
||||||
|
}{
|
||||||
|
{[]string{}, true}, // bare drover.exe → GUI mode → needs admin
|
||||||
|
{[]string{"check"}, false}, // diagnostic only, no driver
|
||||||
|
{[]string{"check", "--host", "x"}, false},
|
||||||
|
{[]string{"--version"}, false},
|
||||||
|
{[]string{"version"}, false},
|
||||||
|
{[]string{"update"}, false}, // self-update doesn't need driver
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := CmdNeedsAdmin(c.args)
|
||||||
|
if got != c.needsAdm {
|
||||||
|
t.Errorf("CmdNeedsAdmin(%v) = %v, want %v", c.args, got, c.needsAdm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user