go-package-manager/privilege.go
2026-07-01 17:47:49 -05:00

92 lines
3.4 KiB
Go

package pkgmgr
import (
"context"
"fmt"
"os"
"os/exec"
)
// dropPrivilege carries the configuration for managers whose command must not
// run as root (Homebrew, AUR helpers). Such managers embed it to gain
// SetDropUser and the shared unprivileged-invocation logic.
type dropPrivilege struct {
dropUser string // user to drop to when running as root; "" falls back to SUDO_USER.
}
// SetDropUser sets the unprivileged user the command drops to when the process
// runs as root. When unset, the SUDO_USER environment variable is used.
func (d *dropPrivilege) SetDropUser(user string) {
d.dropUser = user
}
// dropTarget returns the unprivileged user to drop to, preferring an explicitly
// configured user over SUDO_USER. It returns "" when neither is set.
func (d *dropPrivilege) dropTarget() string {
if d.dropUser != "" {
return d.dropUser
}
return os.Getenv("SUDO_USER")
}
// nonRootInvocation resolves the command and args for running bin, which must
// not execute as root. When not running as root bin is run directly; when
// running as root it is run as dropUser via sudo so it never executes
// privileged. asRoot, the resolved binaries, and dropUser are passed in so the
// decision can be tested.
func nonRootInvocation(bin, sudoBin, dropUser string, asRoot bool, args []string) (string, []string, error) {
if !asRoot {
return bin, args, nil
}
if dropUser == "" {
return "", nil, fmt.Errorf("running as root and no unprivileged user is set; set SUDO_USER or call SetDropUser")
}
if dropUser == "root" {
return "", nil, fmt.Errorf("the configured drop user is root, which is not permitted for this manager")
}
if sudoBin == "" {
return "", nil, fmt.Errorf("unable to find sudo to drop privileges")
}
// -H gives the command the target user's HOME so user-level tooling works.
out := append([]string{"-u", dropUser, "-H", bin}, args...)
return sudoBin, out, nil
}
// commandUnprivileged builds an *exec.Cmd for bin that runs unprivileged,
// dropping to dropUser via sudo when the process is root. It deliberately
// bypasses the sudo command wrapper, since these managers escalate (or refuse)
// on their own. The configured I/O streams are applied.
func (p *baseManager) commandUnprivileged(ctx context.Context, bin, dropUser string, args ...string) (*exec.Cmd, error) {
command, cargs, err := nonRootInvocation(bin, findBinary("sudo"), dropUser, os.Geteuid() == 0, args)
if err != nil {
return nil, err
}
cmd := exec.CommandContext(ctx, command, cargs...)
cmd.Env = os.Environ()
cmd.Stdin = p.stdinOrDefault()
cmd.Stdout = p.stdoutOrDefault()
cmd.Stderr = p.stderrOrDefault()
return cmd, nil
}
// runUnprivileged runs bin with args unprivileged, dropping to dropUser via
// sudo when the process is root.
func (p *baseManager) runUnprivileged(ctx context.Context, bin, dropUser string, args ...string) error {
cmd, err := p.commandUnprivileged(ctx, bin, dropUser, args...)
if err != nil {
return err
}
return cmd.Run()
}
// UseSudoWhenNeeded sets the command wrapper to sudo when the process is not
// already root, so privileged operations escalate. It is a no-op when running
// as root. Managers that must not run as root (brew, yay, paru) bypass the
// wrapper for their own command, so this only affects their privileged
// pacman-side helpers and the root-required managers.
func (p *baseManager) UseSudoWhenNeeded() {
if os.Geteuid() != 0 {
p.SetCmdWrapper([]string{"sudo"})
}
}