feat(su): use alternative privilege elevators when sudo is not available

When sudobin/custom wrapper is not available try the following in order:

- sudo
- doas
- pkexec
- su
This commit is contained in:
jguer 2021-08-31 01:40:43 +02:00 committed by J Guerreiro
parent e43c712c84
commit c8fcdeae5b
5 changed files with 212 additions and 11 deletions

10
main.go
View file

@ -105,7 +105,7 @@ func main() {
config, err = settings.NewConfig(yayVersion) config, err = settings.NewConfig(yayVersion)
if err != nil { if err != nil {
if str := err.Error(); str != "" { if str := err.Error(); str != "" {
fmt.Fprintln(os.Stderr, str) text.Errorln(str)
} }
ret = 1 ret = 1
@ -117,7 +117,7 @@ func main() {
if err = config.ParseCommandLine(cmdArgs); err != nil { if err = config.ParseCommandLine(cmdArgs); err != nil {
if str := err.Error(); str != "" { if str := err.Error(); str != "" {
fmt.Fprintln(os.Stderr, str) text.Errorln(str)
} }
ret = 1 ret = 1
@ -127,7 +127,7 @@ func main() {
if config.Runtime.SaveConfig { if config.Runtime.SaveConfig {
if errS := config.Save(config.Runtime.ConfigPath); errS != nil { if errS := config.Save(config.Runtime.ConfigPath); errS != nil {
fmt.Fprintln(os.Stderr, err) text.Errorln(errS)
} }
} }
@ -136,7 +136,7 @@ func main() {
config.Runtime.PacmanConf, useColor, err = initAlpm(cmdArgs, config.PacmanConf) config.Runtime.PacmanConf, useColor, err = initAlpm(cmdArgs, config.PacmanConf)
if err != nil { if err != nil {
if str := err.Error(); str != "" { if str := err.Error(); str != "" {
fmt.Fprintln(os.Stderr, str) text.Errorln(str)
} }
ret = 1 ret = 1
@ -151,7 +151,7 @@ func main() {
dbExecutor, err := ialpm.NewExecutor(config.Runtime.PacmanConf) dbExecutor, err := ialpm.NewExecutor(config.Runtime.PacmanConf)
if err != nil { if err != nil {
if str := err.Error(); str != "" { if str := err.Error(); str != "" {
fmt.Fprintln(os.Stderr, str) text.Errorln(str)
} }
ret = 1 ret = 1

View file

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
@ -147,6 +148,28 @@ func (c *Configuration) String() string {
return buf.String() return buf.String()
} }
// check privilege elevator exists otherwise try to find another one.
func (c *Configuration) setPrivilegeElevator() error {
for _, bin := range [...]string{c.SudoBin, "sudo"} {
if _, err := exec.LookPath(bin); err == nil {
c.SudoBin = bin
return nil // wrapper or sudo command existing. Retrocompatiblity
}
}
c.SudoFlags = ""
c.SudoLoop = false
for _, bin := range [...]string{"doas", "pkexec", "su"} {
if _, err := exec.LookPath(bin); err == nil {
c.SudoBin = bin
return nil // command existing
}
}
return &ErrPrivilegeElevatorNotFound{confValue: c.SudoBin}
}
func DefaultConfig() *Configuration { func DefaultConfig() *Configuration {
return &Configuration{ return &Configuration{
AURURL: "https://aur.archlinux.org", AURURL: "https://aur.archlinux.org",
@ -208,6 +231,11 @@ func NewConfig(version string) (*Configuration, error) {
newConfig.expandEnv() newConfig.expandEnv()
errPE := newConfig.setPrivilegeElevator()
if errPE != nil {
return nil, errPE
}
newConfig.Runtime = &Runtime{ newConfig.Runtime = &Runtime{
ConfigPath: configPath, ConfigPath: configPath,
Mode: parser.ModeAny, Mode: parser.ModeAny,
@ -277,7 +305,7 @@ func (c *Configuration) CmdBuilder(runner exe.Runner) exe.ICmdBuilder {
MakepkgBin: c.MakepkgBin, MakepkgBin: c.MakepkgBin,
SudoBin: c.SudoBin, SudoBin: c.SudoBin,
SudoFlags: strings.Fields(c.SudoFlags), SudoFlags: strings.Fields(c.SudoFlags),
SudoLoopEnabled: false, SudoLoopEnabled: c.SudoLoop,
PacmanBin: c.PacmanBin, PacmanBin: c.PacmanBin,
PacmanConfigPath: c.PacmanConf, PacmanConfigPath: c.PacmanConf,
PacmanDBPath: "", PacmanDBPath: "",

153
pkg/settings/config_test.go Normal file
View file

@ -0,0 +1,153 @@
package settings
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
// GIVEN default config
// WHEN setPrivilegeElevator gets called
// THEN sudobin should stay as "sudo" (given sudo exists)
func TestConfiguration_setPrivilegeElevator(t *testing.T) {
oldPath := os.Getenv("PATH")
path, err := os.MkdirTemp("", "yay-test")
assert.NoError(t, err)
doas := filepath.Join(path, "sudo")
_, err = os.Create(doas)
os.Chmod(doas, 0o755)
assert.NoError(t, err)
defer os.RemoveAll(path)
config := DefaultConfig()
config.SudoLoop = true
config.SudoFlags = "-v"
os.Setenv("PATH", path)
err = config.setPrivilegeElevator()
os.Setenv("PATH", oldPath)
assert.NoError(t, err)
assert.Equal(t, "sudo", config.SudoBin)
assert.Equal(t, "-v", config.SudoFlags)
assert.True(t, config.SudoLoop)
}
// GIVEN default config and sudo loop enabled
// GIVEN only su in path
// WHEN setPrivilegeElevator gets called
// THEN sudobin should be changed to "su"
func TestConfiguration_setPrivilegeElevator_su(t *testing.T) {
oldPath := os.Getenv("PATH")
path, err := os.MkdirTemp("", "yay-test")
assert.NoError(t, err)
doas := filepath.Join(path, "su")
_, err = os.Create(doas)
os.Chmod(doas, 0o755)
assert.NoError(t, err)
defer os.RemoveAll(path)
config := DefaultConfig()
config.SudoLoop = true
config.SudoFlags = "-v"
os.Setenv("PATH", path)
err = config.setPrivilegeElevator()
os.Setenv("PATH", oldPath)
assert.NoError(t, err)
assert.Equal(t, "su", config.SudoBin)
assert.Equal(t, "", config.SudoFlags)
assert.False(t, config.SudoLoop)
}
// GIVEN default config and sudo loop enabled
// GIVEN no sudo in path
// WHEN setPrivilegeElevator gets called
// THEN sudobin should be changed to "su"
func TestConfiguration_setPrivilegeElevator_no_path(t *testing.T) {
oldPath := os.Getenv("PATH")
os.Setenv("PATH", "")
config := DefaultConfig()
config.SudoLoop = true
config.SudoFlags = "-v"
err := config.setPrivilegeElevator()
os.Setenv("PATH", oldPath)
assert.Error(t, err)
assert.Equal(t, "sudo", config.SudoBin)
assert.Equal(t, "", config.SudoFlags)
assert.False(t, config.SudoLoop)
}
// GIVEN default config and sudo loop enabled
// GIVEN doas in path
// WHEN setPrivilegeElevator gets called
// THEN sudobin should be changed to "doas"
func TestConfiguration_setPrivilegeElevator_doas(t *testing.T) {
oldPath := os.Getenv("PATH")
path, err := os.MkdirTemp("", "yay-test")
assert.NoError(t, err)
doas := filepath.Join(path, "doas")
_, err = os.Create(doas)
os.Chmod(doas, 0o755)
assert.NoError(t, err)
defer os.RemoveAll(path)
config := DefaultConfig()
config.SudoLoop = true
config.SudoFlags = "-v"
os.Setenv("PATH", path)
err = config.setPrivilegeElevator()
os.Setenv("PATH", oldPath)
assert.NoError(t, err)
assert.Equal(t, "doas", config.SudoBin)
assert.Equal(t, "", config.SudoFlags)
assert.False(t, config.SudoLoop)
}
// GIVEN config with wrapper and sudo loop enabled
// GIVEN wrapper is in path
// WHEN setPrivilegeElevator gets called
// THEN sudobin should be kept as the wrapper
func TestConfiguration_setPrivilegeElevator_custom_script(t *testing.T) {
oldPath := os.Getenv("PATH")
path, err := os.MkdirTemp("", "yay-test")
assert.NoError(t, err)
wrapper := filepath.Join(path, "custom-wrapper")
_, err = os.Create(wrapper)
os.Chmod(wrapper, 0o755)
assert.NoError(t, err)
defer os.RemoveAll(path)
config := DefaultConfig()
config.SudoLoop = true
config.SudoBin = wrapper
config.SudoFlags = "-v"
os.Setenv("PATH", path)
err = config.setPrivilegeElevator()
os.Setenv("PATH", oldPath)
assert.NoError(t, err)
assert.Equal(t, wrapper, config.SudoBin)
assert.Equal(t, "-v", config.SudoFlags)
assert.True(t, config.SudoLoop)
}

11
pkg/settings/errors.go Normal file
View file

@ -0,0 +1,11 @@
package settings
import "fmt"
type ErrPrivilegeElevatorNotFound struct {
confValue string
}
func (e *ErrPrivilegeElevatorNotFound) Error() string {
return fmt.Sprintf("unable to find a privilege elevator, config value: %s", e.confValue)
}

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
@ -91,15 +92,22 @@ func (c *CmdBuilder) SetPacmanDBPath(dbPath string) {
c.PacmanDBPath = dbPath c.PacmanDBPath = dbPath
} }
func (c *CmdBuilder) buildPrivilegeElevatorCommand(ctx context.Context, ogArgs []string) *exec.Cmd {
if c.SudoBin == "su" {
return exec.CommandContext(ctx, c.SudoBin, "-c", strings.Join(ogArgs, " "))
}
argArr := make([]string, 0, len(c.SudoFlags)+len(ogArgs))
argArr = append(argArr, c.SudoFlags...)
argArr = append(argArr, ogArgs...)
return exec.CommandContext(ctx, c.SudoBin, argArr...)
}
func (c *CmdBuilder) BuildPacmanCmd(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd { func (c *CmdBuilder) BuildPacmanCmd(ctx context.Context, args *parser.Arguments, mode parser.TargetMode, noConfirm bool) *exec.Cmd {
argArr := make([]string, 0, 32) argArr := make([]string, 0, 32)
needsRoot := args.NeedRoot(mode) needsRoot := args.NeedRoot(mode)
if needsRoot {
argArr = append(argArr, c.SudoBin)
argArr = append(argArr, c.SudoFlags...)
}
argArr = append(argArr, c.PacmanBin) argArr = append(argArr, c.PacmanBin)
argArr = append(argArr, args.FormatGlobals()...) argArr = append(argArr, args.FormatGlobals()...)
argArr = append(argArr, args.FormatArgs()...) argArr = append(argArr, args.FormatArgs()...)
@ -113,6 +121,7 @@ func (c *CmdBuilder) BuildPacmanCmd(ctx context.Context, args *parser.Arguments,
if needsRoot { if needsRoot {
waitLock(c.PacmanDBPath) waitLock(c.PacmanDBPath)
return c.buildPrivilegeElevatorCommand(ctx, argArr)
} }
return exec.CommandContext(ctx, argArr[0], argArr[1:]...) return exec.CommandContext(ctx, argArr[0], argArr[1:]...)