main bac21c6

runtime_guard_test.go RawHistoryFolder

1package app
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/gordon/internal/adapters/out/secretfiles"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15func fakeEnv(values map[string]string) func(string) string {
16	return func(key string) string { return values[key] }
17}
18
19func TestProtectedRuntimePaths_AllSources(t *testing.T) {
20	home := t.TempDir()
21	env := fakeEnv(map[string]string{
22		"XDG_RUNTIME_DIR":    "/run/user/1000",
23		"DOCKER_HOST":        "unix:///x/d.sock",
24		"GNUPGHOME":          "/keys/gnupg",
25		"PASSWORD_STORE_DIR": "/keys/pass",
26		"SOPS_AGE_KEY_FILE":  "/keys/age.txt",
27		"XDG_CONFIG_HOME":    "/cfg",
28
29		"GORDON_CLOUDFLARE_API_TOKEN_FILE": "/keys/cf-token",
30	})
31	got := protectedRuntimePaths("/data/gordon", []string{"/etc/gordon/gordon.toml", "/custom/podman.sock", ""}, env, home)
32
33	for _, want := range []string{
34		"/data/gordon",
35		"/etc/gordon/gordon.toml",
36		"/var/run/docker.sock",
37		"/run/docker.sock",
38		"/run/podman/podman.sock",
39		"/run/user/1000/docker.sock",
40		"/run/user/1000/podman/podman.sock",
41		"/run/user/1000/gordon",
42		filepath.Join(home, ".gordon", "run"),
43		"/x/d.sock",
44		"/custom/podman.sock",
45		"/keys/gnupg",
46		filepath.Join(home, ".gnupg"),
47		"/keys/pass",
48		filepath.Join(home, ".password-store"),
49		"/keys/age.txt",
50		"/cfg/sops/age",
51		"/keys/cf-token",
52		filepath.Join(home, ".local", "share", "containers"),
53		filepath.Join(home, ".local", "share", "docker"),
54		"/cfg/containers",
55		filepath.Join(home, ".docker"),
56		"/cfg/systemd/user",
57	} {
58		assert.Contains(t, got, want)
59	}
60	assert.IsIncreasing(t, got)
61}
62
63func TestProtectedRuntimePaths_SkipsEmptyAndTCP(t *testing.T) {
64	// Subset, not Equal: on hosts where a fixed path crosses a symlink
65	// (for example /var -> /private/var) its resolved form is added too.
66	got := protectedRuntimePaths("/data", nil, fakeEnv(map[string]string{"DOCKER_HOST": "tcp://h:2375"}), "")
67	assert.Subset(t, got, []string{"/data", "/run/docker.sock", "/run/podman/podman.sock", "/var/run/docker.sock"})
68	for _, p := range got {
69		assert.NotContains(t, p, "tcp:")
70		assert.NotContains(t, p, "h:2375")
71	}
72}
73
74func TestProtectedRuntimePaths_SymlinkAddsBothForms(t *testing.T) {
75	base := t.TempDir()
76	real := filepath.Join(base, "real")
77	require.NoError(t, os.Mkdir(real, 0o700))
78	link := filepath.Join(base, "link")
79	require.NoError(t, os.Symlink(real, link))
80	realResolved, err := filepath.EvalSymlinks(real)
81	require.NoError(t, err)
82
83	got := protectedRuntimePaths(link, nil, fakeEnv(nil), "")
84	assert.Contains(t, got, link)
85	assert.Contains(t, got, realResolved)
86
87	// A protected path that does not exist yet under a symlinked parent
88	// is protected in its resolved form too.
89	missing := filepath.Join(link, "later", "store")
90	got = protectedRuntimePaths("/data", []string{missing}, fakeEnv(nil), "")
91	assert.Contains(t, got, missing)
92	assert.Contains(t, got, filepath.Join(realResolved, "later", "store"))
93}
94
95func TestConfiguredGordonPaths(t *testing.T) {
96	si := &serviceInit{}
97	si.cfg.Server.TLSKeyFile = "/tls/key.pem"
98	si.cfg.Server.DataDir = "/data"
99	si.cfg.Logging.AccessLog.FilePath = "/logs/access.log"
100	si.cfg.Backups.Databases.StorageDir = "/backups"
101
102	got := configuredGordonPaths(si)
103	assert.Contains(t, got, "/tls/key.pem")
104	assert.Contains(t, got, "/backups")
105	assert.NotContains(t, got, "/logs/access.log")
106}
107
108func TestGordonLogLocations(t *testing.T) {
109	var cfg Config
110	cfg.Server.DataDir = "/data"
111	cfg.Logging.AccessLog.FilePath = "/logs/access.log"
112	assert.Empty(t, gordonLogLocations(cfg, "/data"), "disabled logs grant nothing")
113
114	cfg.Logging.File.Enabled = true
115	cfg.Logging.AccessLog.Enabled = true
116	cfg.Logging.AccessLog.Output = "file"
117	cfg.Logging.ContainerLogs.Enabled = true
118	assert.ElementsMatch(t, []logLocation{
119		{dir: "/data/logs", path: "/data/logs/gordon.log"},
120		{dir: "/logs", path: "/logs/access.log"},
121		{dir: "/data/logs/containers", path: "/data/logs/containers"},
122	}, gordonLogLocations(cfg, "/data"))
123
124	cfg.Logging.AccessLog.Output = "stdout"
125	assert.Len(t, gordonLogLocations(cfg, "/data"), 2, "non-file access log grants nothing")
126}
127
128func TestReadableLogDirs(t *testing.T) {
129	protected := []string{"/data", "/home/u", "/home/u/.gnupg", "/srv/logs/keys"}
130	loc := func(dir string) logLocation { return logLocation{dir: dir, path: dir + "/x.log"} }
131	readable, keep := readableLogDirs([]logLocation{
132		loc("/data/logs"),
133		loc("/data/logs/containers"),
134		loc("/data"),              // data dir root
135		loc("/data/secrets-run"),  // data child outside logs
136		loc("/data/secrets"),      // data child outside logs
137		loc("/home/u"),            // protected itself
138		loc("/home/u/.gnupg/log"), // under a key store
139		loc("/srv/logs"),          // contains a protected path
140		loc("/srv"),               // ancestor of a protected path
141		loc("/var/log/gordon"),
142	}, protected, "/data")
143	assert.Equal(t, []string{"/data/logs", "/data/logs/containers", "/var/log/gordon"}, readable)
144	assert.Equal(t, []string{
145		"/data/x.log", "/data/secrets-run/x.log", "/data/secrets/x.log", "/home/u/x.log",
146		"/home/u/.gnupg/log/x.log", "/srv/logs/x.log", "/srv/x.log",
147	}, keep)
148}
149
150func TestBuildAppRuntimeSafety_DroppedLogStaysProtected(t *testing.T) {
151	home := t.TempDir()
152	t.Setenv("HOME", home)
153	si := &serviceInit{}
154	si.cfg.Logging.File.Enabled = true
155	si.cfg.Logging.File.Path = filepath.Join(home, "gordon.log")
156	store, guard, err := buildAppRuntimeSafety(si, t.TempDir())
157	require.NoError(t, err)
158	t.Cleanup(func() { _ = store.(*secretfiles.Store).Close() })
159
160	// $HOME contains key stores, so it is no readable log dir, and the
161	// log file itself stays protected even against a writable bind.
162	assert.NotContains(t, guard.LogDirs, home)
163	err = guard.Check(&domain.ContainerConfig{Binds: []domain.ContainerBind{{Name: "log", Source: filepath.Join(home, "gordon.log"), Destination: "/log"}}})
164	require.ErrorIs(t, err, domain.ErrRuntimeGuard)
165}
166
167func TestBuildAppRuntimeSafety(t *testing.T) {
168	dataDir := t.TempDir()
169	si := &serviceInit{}
170	si.cfg.Logging.ContainerLogs.Enabled = true
171	si.cfg.Logging.File.Enabled = true
172	store, guard, err := buildAppRuntimeSafety(si, dataDir)
173	require.NoError(t, err)
174	require.NotNil(t, store)
175	t.Cleanup(func() { _ = store.(*secretfiles.Store).Close() })
176	assert.Equal(t, filepath.Join(dataDir, "secrets-run"), guard.SecretFilesDir)
177	assert.Contains(t, guard.ProtectedPaths, dataDir)
178
179	// The data dir itself cannot be bound into an app.
180	err = guard.Check(&domain.ContainerConfig{Binds: []domain.ContainerBind{{Name: "all", Source: dataDir, Destination: "/data"}}})
181	require.ErrorIs(t, err, domain.ErrRuntimeGuard)
182
183	// The default log dir under the data dir is readable, never writable.
184	logs := filepath.Join(dataDir, "logs")
185	require.NoError(t, guard.Check(&domain.ContainerConfig{Binds: []domain.ContainerBind{{Name: "logs", Source: logs, Destination: "/var/log/gordon", ReadOnly: true}}}))
186	err = guard.Check(&domain.ContainerConfig{Binds: []domain.ContainerBind{{Name: "logs", Source: logs, Destination: "/var/log/gordon"}}})
187	require.ErrorIs(t, err, domain.ErrRuntimeGuard)
188}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close