Kaynağa Gözat

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
jguer 3 yıl önce
ebeveyn
işleme
c8fcdeae5b

+ 5 - 5
main.go

@@ -105,7 +105,7 @@ func main() {
 	config, err = settings.NewConfig(yayVersion)
 	if err != nil {
 		if str := err.Error(); str != "" {
-			fmt.Fprintln(os.Stderr, str)
+			text.Errorln(str)
 		}
 
 		ret = 1
@@ -117,7 +117,7 @@ func main() {
 
 	if err = config.ParseCommandLine(cmdArgs); err != nil {
 		if str := err.Error(); str != "" {
-			fmt.Fprintln(os.Stderr, str)
+			text.Errorln(str)
 		}
 
 		ret = 1
@@ -127,7 +127,7 @@ func main() {
 
 	if config.Runtime.SaveConfig {
 		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)
 	if err != nil {
 		if str := err.Error(); str != "" {
-			fmt.Fprintln(os.Stderr, str)
+			text.Errorln(str)
 		}
 
 		ret = 1
@@ -151,7 +151,7 @@ func main() {
 	dbExecutor, err := ialpm.NewExecutor(config.Runtime.PacmanConf)
 	if err != nil {
 		if str := err.Error(); str != "" {
-			fmt.Fprintln(os.Stderr, str)
+			text.Errorln(str)
 		}
 
 		ret = 1

+ 29 - 1
pkg/settings/config.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net/http"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
 
@@ -147,6 +148,28 @@ func (c *Configuration) String() 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 {
 	return &Configuration{
 		AURURL:             "https://aur.archlinux.org",
@@ -208,6 +231,11 @@ func NewConfig(version string) (*Configuration, error) {
 
 	newConfig.expandEnv()
 
+	errPE := newConfig.setPrivilegeElevator()
+	if errPE != nil {
+		return nil, errPE
+	}
+
 	newConfig.Runtime = &Runtime{
 		ConfigPath:     configPath,
 		Mode:           parser.ModeAny,
@@ -277,7 +305,7 @@ func (c *Configuration) CmdBuilder(runner exe.Runner) exe.ICmdBuilder {
 		MakepkgBin:       c.MakepkgBin,
 		SudoBin:          c.SudoBin,
 		SudoFlags:        strings.Fields(c.SudoFlags),
-		SudoLoopEnabled:  false,
+		SudoLoopEnabled:  c.SudoLoop,
 		PacmanBin:        c.PacmanBin,
 		PacmanConfigPath: c.PacmanConf,
 		PacmanDBPath:     "",

+ 153 - 0
pkg/settings/config_test.go

@@ -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 - 0
pkg/settings/errors.go

@@ -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)
+}

+ 14 - 5
pkg/settings/exe/cmd_builder.go

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