Skip to content

Commit 7fe260f

Browse files
authored
internal/nix/config: generalize config parsing (#1988)
Add a `nix.CurrentConfig` function that calls `nix show-config --json` and unmarshals the result into a `nix.Config` struct. Move the `nixcache.IsUserTrusted` function to `nix.Config.IsUserTrusted` and implement handling of user groups.
1 parent fee7913 commit 7fe260f

File tree

4 files changed

+215
-27
lines changed

4 files changed

+215
-27
lines changed

internal/devbox/providers/nixcache/nixcache.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,12 @@ func checkIfUserCanAddSubstituter(ctx context.Context) {
223223
// * Also check if cache is enabled in nix.conf
224224
// * Test on single user install
225225
// * Automate making user trusted if needed
226-
if !nix.IsUserTrusted(ctx) {
226+
cfg, err := nix.CurrentConfig(ctx)
227+
if err != nil {
228+
return
229+
}
230+
trusted, _ := cfg.IsUserTrusted(ctx)
231+
if !trusted {
227232
ux.Fwarning(
228233
os.Stderr,
229234
"In order to use a custom nix cache you must be a trusted user. Please "+

internal/nix/config.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package nix
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"os/exec"
8+
"os/user"
9+
"slices"
10+
"strings"
11+
12+
"go.jetpack.io/devbox/internal/debug"
13+
"go.jetpack.io/devbox/internal/redact"
14+
)
15+
16+
// Config is a parsed Nix configuration.
17+
type Config struct {
18+
ExperimentalFeatures ConfigField[[]string] `json:"experimental-features"`
19+
Substitute ConfigField[bool] `json:"substitute"`
20+
Substituters ConfigField[[]string] `json:"substituters"`
21+
System ConfigField[string] `json:"system"`
22+
TrustedSubstituters ConfigField[[]string] `json:"trusted-substituters"`
23+
TrustedUsers ConfigField[[]string] `json:"trusted-users"`
24+
}
25+
26+
// ConfigField is a Nix configuration setting.
27+
type ConfigField[T any] struct {
28+
Value T `json:"value"`
29+
}
30+
31+
// CurrentConfig reads the current Nix configuration.
32+
func CurrentConfig(ctx context.Context) (Config, error) {
33+
// `nix show-config` is deprecated in favor of `nix config show`, but we
34+
// want to remain compatible with older Nix versions.
35+
cmd := commandContext(ctx, "show-config", "--json")
36+
out, err := cmd.Output()
37+
var exitErr *exec.ExitError
38+
if errors.As(err, &exitErr) && len(exitErr.Stderr) != 0 {
39+
return Config{}, redact.Errorf("command %s: %v: %s", redact.Safe(cmd), err, exitErr.Stderr)
40+
}
41+
if err != nil {
42+
return Config{}, redact.Errorf("command %s: %v", cmd, err)
43+
}
44+
cfg := Config{}
45+
if err := json.Unmarshal(out, &cfg); err != nil {
46+
return Config{}, redact.Errorf("unmarshal JSON output from %s: %v", redact.Safe(cmd), err)
47+
}
48+
return cfg, nil
49+
}
50+
51+
// IsUserTrusted reports if the current OS user is in the trusted-users list. If
52+
// there are any groups in the list, it also checks if the user belongs to any
53+
// of them.
54+
func (c Config) IsUserTrusted(ctx context.Context) (bool, error) {
55+
trusted := c.TrustedUsers.Value
56+
if len(trusted) == 0 {
57+
return false, nil
58+
}
59+
60+
current, err := user.Current()
61+
if err != nil {
62+
return false, redact.Errorf("lookup current user: %v", err)
63+
}
64+
if slices.Contains(trusted, current.Username) {
65+
return true, nil
66+
}
67+
68+
// trusted-user entries that start with an @ are group names
69+
// (for example, @wheel). Lookup each group ID to see if the user
70+
// belongs to a trusted group.
71+
var currentGids []string
72+
for i := range trusted {
73+
groupName := strings.TrimPrefix(trusted[i], "@")
74+
if groupName == trusted[i] || groupName == "" {
75+
continue
76+
}
77+
78+
group, err := user.LookupGroup(groupName)
79+
var unknownErr user.UnknownGroupError
80+
if errors.As(err, &unknownErr) {
81+
debug.Log("skipping unknown trusted-user group %q found in nix.conf", groupName)
82+
continue
83+
}
84+
if err != nil {
85+
return false, redact.Errorf("lookup trusted-user group from nix.conf: %v", err)
86+
}
87+
88+
// Be lazy about looking up the current user's groups until we
89+
// encounter one in the trusted-users list.
90+
if currentGids == nil {
91+
currentGids, err = current.GroupIds()
92+
if err != nil {
93+
return false, redact.Errorf("lookup current user group IDs: %v", err)
94+
}
95+
}
96+
if slices.Contains(currentGids, group.Gid) {
97+
return true, nil
98+
}
99+
}
100+
return false, nil
101+
}

internal/nix/config_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package nix
2+
3+
import (
4+
"context"
5+
"os/user"
6+
"testing"
7+
)
8+
9+
//nolint:revive
10+
func TestConfigIsUserTrusted(t *testing.T) {
11+
t.Run("UsernameInList", func(t *testing.T) {
12+
u, err := user.Current()
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
t.Setenv("NIX_CONFIG", "trusted-users = "+u.Username)
17+
18+
ctx := context.Background()
19+
cfg, err := CurrentConfig(ctx)
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
24+
trusted, err := cfg.IsUserTrusted(ctx)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
if !trusted {
29+
t.Error("got trusted = false, want true")
30+
}
31+
})
32+
t.Run("UserGroupInList", func(t *testing.T) {
33+
u, err := user.Current()
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
g, err := user.LookupGroupId(u.Gid)
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
t.Setenv("NIX_CONFIG", "trusted-users = @"+g.Name)
42+
43+
ctx := context.Background()
44+
cfg, err := CurrentConfig(ctx)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
trusted, err := cfg.IsUserTrusted(ctx)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
if !trusted {
54+
t.Error("got trusted = false, want true")
55+
}
56+
})
57+
t.Run("NotInList", func(t *testing.T) {
58+
t.Setenv("NIX_CONFIG", "trusted-users = root")
59+
60+
ctx := context.Background()
61+
cfg, err := CurrentConfig(ctx)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
66+
trusted, err := cfg.IsUserTrusted(ctx)
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
if trusted {
71+
t.Error("got trusted = true, want false")
72+
}
73+
})
74+
t.Run("EmptyList", func(t *testing.T) {
75+
t.Setenv("NIX_CONFIG", "trusted-users =")
76+
77+
ctx := context.Background()
78+
cfg, err := CurrentConfig(ctx)
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
83+
trusted, err := cfg.IsUserTrusted(ctx)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
if trusted {
88+
t.Error("got trusted = true, want false")
89+
}
90+
})
91+
t.Run("UnknownGroup", func(t *testing.T) {
92+
t.Setenv("NIX_CONFIG", "trusted-users = @dummygroup")
93+
94+
ctx := context.Background()
95+
cfg, err := CurrentConfig(ctx)
96+
if err != nil {
97+
t.Fatal(err)
98+
}
99+
100+
trusted, err := cfg.IsUserTrusted(ctx)
101+
if err != nil {
102+
t.Fatal(err)
103+
}
104+
if trusted {
105+
t.Error("got trusted = true, want false")
106+
}
107+
})
108+
}

internal/nix/nix.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ import (
1010
"io/fs"
1111
"os"
1212
"os/exec"
13-
"os/user"
1413
"path/filepath"
1514
"regexp"
1615
"runtime/trace"
17-
"slices"
1816
"strings"
1917
"sync"
2018
"time"
@@ -414,27 +412,3 @@ func parseInsecurePackagesFromExitError(errorMsg string) []string {
414412

415413
return insecurePackages
416414
}
417-
418-
func IsUserTrusted(ctx context.Context) bool {
419-
cmd := commandContext(ctx, "show-config", "--json")
420-
out, err := cmd.Output()
421-
if err != nil {
422-
return false
423-
}
424-
425-
var config struct {
426-
TrustedUsers struct {
427-
Value []string `json:"value"`
428-
} `json:"trusted-users"`
429-
}
430-
if err := json.Unmarshal(out, &config); err != nil {
431-
return false
432-
}
433-
434-
u, err := user.Current()
435-
if err != nil {
436-
return false
437-
}
438-
439-
return slices.Contains(config.TrustedUsers.Value, u.Username)
440-
}

0 commit comments

Comments
 (0)