92 lines
3.4 KiB
Go
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"})
|
|
}
|
|
}
|