Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/networks/sudoers.go
2601 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package networks
5
6
import (
7
"context"
8
"errors"
9
"fmt"
10
"os"
11
"os/exec"
12
"slices"
13
"strings"
14
15
"github.com/sirupsen/logrus"
16
)
17
18
func Sudoers() (string, error) {
19
cfg, err := LoadConfig()
20
if err != nil {
21
return "", err
22
}
23
24
var sb strings.Builder
25
sb.WriteString(fmt.Sprintf("%%%s ALL=(root:wheel) NOPASSWD:NOSETENV: %s\n", cfg.Group, cfg.MkdirCmd()))
26
27
// names must be in stable order to be able to check if sudoers file needs updating
28
names := make([]string, 0, len(cfg.Networks))
29
for name, nw := range cfg.Networks {
30
if nw.Mode == ModeUserV2 {
31
continue // no sudo needed
32
}
33
names = append(names, name)
34
}
35
slices.Sort(names)
36
37
for _, name := range names {
38
sb.WriteRune('\n')
39
sb.WriteString(fmt.Sprintf("# Manage %q network daemons\n", name))
40
for _, daemon := range []string{SocketVMNet} {
41
if ok, err := cfg.IsDaemonInstalled(daemon); err != nil {
42
return "", err
43
} else if !ok {
44
continue
45
}
46
user, err := cfg.User(daemon)
47
if err != nil {
48
return "", err
49
}
50
sb.WriteRune('\n')
51
sb.WriteString(fmt.Sprintf("%%%s ALL=(%s:%s) NOPASSWD:NOSETENV: \\\n",
52
cfg.Group, user.User, user.Group))
53
sb.WriteString(fmt.Sprintf(" %s, \\\n", cfg.StartCmd(name, daemon)))
54
sb.WriteString(fmt.Sprintf(" %s\n", cfg.StopCmd(name, daemon)))
55
}
56
}
57
return sb.String(), nil
58
}
59
60
func (c *Config) passwordLessSudo(ctx context.Context) error {
61
// Flush cached sudo password
62
cmd := exec.CommandContext(ctx, "sudo", "-k")
63
if err := cmd.Run(); err != nil {
64
return fmt.Errorf("failed to run %v: %w", cmd.Args, err)
65
}
66
// Verify that user/groups for both daemons work without a password, e.g.
67
// %admin ALL = (ALL:ALL) NOPASSWD: ALL
68
for _, daemon := range []string{SocketVMNet} {
69
if ok, err := c.IsDaemonInstalled(daemon); err != nil {
70
return err
71
} else if !ok {
72
continue
73
}
74
user, err := c.User(daemon)
75
if err != nil {
76
return err
77
}
78
cmd = exec.CommandContext(ctx, "sudo", "--user", user.User, "--group", user.Group, "--non-interactive", "true")
79
if err := cmd.Run(); err != nil {
80
return fmt.Errorf("failed to run %v: %w", cmd.Args, err)
81
}
82
}
83
return nil
84
}
85
86
func (c *Config) VerifySudoAccess(ctx context.Context, sudoersFile string) error {
87
if sudoersFile == "" {
88
err := c.passwordLessSudo(ctx)
89
if err == nil {
90
logrus.Debug("sudo doesn't seem to require a password")
91
return nil
92
}
93
return fmt.Errorf("passwordLessSudo error: %w", err)
94
}
95
hint := fmt.Sprintf("run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)",
96
os.Args[0], sudoersFile)
97
b, err := os.ReadFile(sudoersFile)
98
if err != nil {
99
// Default networks.yaml specifies /etc/sudoers.d/lima file. Don't throw an error when the
100
// file doesn't exist, as long as password-less sudo still works.
101
if errors.Is(err, os.ErrNotExist) {
102
err = c.passwordLessSudo(ctx)
103
if err == nil {
104
logrus.Debugf("%q does not exist, but sudo doesn't seem to require a password", sudoersFile)
105
return nil
106
}
107
logrus.Debugf("%q does not exist; passwordLessSudo error: %s", sudoersFile, err)
108
}
109
return fmt.Errorf("can't read %q: %w: (Hint: %s)", sudoersFile, err, hint)
110
}
111
sudoers, err := Sudoers()
112
if err != nil {
113
return err
114
}
115
if string(b) != sudoers {
116
// Happens on upgrading socket_vmnet with Homebrew
117
return fmt.Errorf("sudoers file %q is out of sync and must be regenerated (Hint: %s)", sudoersFile, hint)
118
}
119
return nil
120
}
121
122