From c8fcdeae5b168d7a3eb3d805ee823d0fd5658b85 Mon Sep 17 00:00:00 2001 From: jguer Date: Tue, 31 Aug 2021 01:40:43 +0200 Subject: [PATCH] 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 --- main.go | 10 +-- pkg/settings/config.go | 30 ++++++- pkg/settings/config_test.go | 153 ++++++++++++++++++++++++++++++++ pkg/settings/errors.go | 11 +++ pkg/settings/exe/cmd_builder.go | 19 ++-- 5 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 pkg/settings/config_test.go create mode 100644 pkg/settings/errors.go diff --git a/main.go b/main.go index 2acddc82..acff145b 100644 --- a/main.go +++ b/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 diff --git a/pkg/settings/config.go b/pkg/settings/config.go index 6d168121..05966199 100644 --- a/pkg/settings/config.go +++ b/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: "", diff --git a/pkg/settings/config_test.go b/pkg/settings/config_test.go new file mode 100644 index 00000000..f0ef70c4 --- /dev/null +++ b/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) +} diff --git a/pkg/settings/errors.go b/pkg/settings/errors.go new file mode 100644 index 00000000..e57316e0 --- /dev/null +++ b/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) +} diff --git a/pkg/settings/exe/cmd_builder.go b/pkg/settings/exe/cmd_builder.go index 329208e1..ace2f3ca 100644 --- a/pkg/settings/exe/cmd_builder.go +++ b/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:]...)