runtime_guard_test.go RawHistoryFolder

1package domain_test
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/domain"
12)
13
14const guardContainer = "gordon-blog--web--x"
15
16func guardConfig(binds ...domain.ContainerBind) *domain.ContainerConfig {
17	return &domain.ContainerConfig{
18		Name:        guardContainer,
19		NetworkMode: "gordon-app-1",
20		Labels:      map[string]string{domain.LabelApp: "blog"},
21		Binds:       binds,
22	}
23}
24
25func TestRuntimeGuard_NetworkModes(t *testing.T) {
26	var guard domain.RuntimeGuard
27	for _, mode := range []string{"host", "container:abc", "ns:/proc/1/ns/net"} {
28		cfg := guardConfig()
29		cfg.NetworkMode = mode
30		err := guard.Check(cfg)
31		require.ErrorIs(t, err, domain.ErrRuntimeGuard, mode)
32		assert.NotContains(t, err.Error(), "/proc")
33	}
34	require.NoError(t, guard.Check(guardConfig()))
35}
36
37func TestRuntimeGuard_VolumeNames(t *testing.T) {
38	var guard domain.RuntimeGuard
39	for _, name := range []string{"/etc", "a/b", `a\b`} {
40		cfg := guardConfig()
41		cfg.Volumes = map[string]string{"/data": name}
42		require.ErrorIs(t, guard.Check(cfg), domain.ErrRuntimeGuard, name)
43
44		cfg = guardConfig()
45		cfg.ReadOnlyVolumes = map[string]string{"/data": name}
46		require.ErrorIs(t, guard.Check(cfg), domain.ErrRuntimeGuard, name)
47	}
48	cfg := guardConfig()
49	cfg.Volumes = map[string]string{"/data": "gordon-blog--web--vol--data"}
50	require.NoError(t, guard.Check(cfg))
51}
52
53func TestRuntimeGuard_ProtectedBinds(t *testing.T) {
54	base := t.TempDir()
55	dataDir := filepath.Join(base, "data")
56	protectedChild := filepath.Join(dataDir, "registry")
57	require.NoError(t, os.MkdirAll(protectedChild, 0o700))
58	unrelated := filepath.Join(base, "public")
59	require.NoError(t, os.MkdirAll(unrelated, 0o700))
60	link := filepath.Join(base, "link")
61	require.NoError(t, os.Symlink(dataDir, link))
62
63	guard := domain.RuntimeGuard{ProtectedPaths: []string{dataDir}}
64	cases := map[string]string{
65		"equal":    dataDir,
66		"under":    protectedChild,
67		"ancestor": base,
68		"symlink":  link,
69	}
70	for name, source := range cases {
71		t.Run(name, func(t *testing.T) {
72			err := guard.Check(guardConfig(domain.ContainerBind{Name: "conf", Source: source, Destination: "/etc/app", ReadOnly: true}))
73			require.ErrorIs(t, err, domain.ErrRuntimeGuard)
74			assert.Contains(t, err.Error(), `"conf"`)
75			assert.NotContains(t, err.Error(), base)
76		})
77	}
78	require.NoError(t, guard.Check(guardConfig(domain.ContainerBind{Name: "conf", Source: unrelated, Destination: "/etc/app", ReadOnly: true})))
79}
80
81func TestRuntimeGuard_OwnSecretBindException(t *testing.T) {
82	dataDir := t.TempDir()
83	secretDir := filepath.Join(dataDir, "secrets-run")
84	guard := domain.RuntimeGuard{ProtectedPaths: []string{dataDir}, SecretFilesDir: secretDir}
85	own := domain.ContainerBind{
86		Name:        "secret-file-DB",
87		Source:      filepath.Join(secretDir, "blog", guardContainer, "DB"),
88		Destination: "/run/secrets/DB",
89		ReadOnly:    true,
90	}
91	require.NoError(t, guard.Check(guardConfig(own)))
92
93	cases := map[string]func(*domain.ContainerConfig){
94		"read-write":   func(c *domain.ContainerConfig) { c.Binds[0].ReadOnly = false },
95		"wrong dest":   func(c *domain.ContainerConfig) { c.Binds[0].Destination = "/run/secrets/OTHER" },
96		"wrong app":    func(c *domain.ContainerConfig) { c.Labels[domain.LabelApp] = "other" },
97		"no app label": func(c *domain.ContainerConfig) { c.Labels = nil },
98		"wrong name":   func(c *domain.ContainerConfig) { c.Name = "gordon-blog--web--y" },
99		"extra segment": func(c *domain.ContainerConfig) {
100			c.Binds[0].Source = filepath.Join(secretDir, "blog", guardContainer, "sub", "DB")
101		},
102		"directory bind": func(c *domain.ContainerConfig) { c.Binds[0].Source = filepath.Join(secretDir, "blog", guardContainer) },
103		"whole base":     func(c *domain.ContainerConfig) { c.Binds[0].Source = secretDir },
104	}
105	for name, mutate := range cases {
106		t.Run(name, func(t *testing.T) {
107			cfg := guardConfig(own)
108			mutate(cfg)
109			err := guard.Check(cfg)
110			require.ErrorIs(t, err, domain.ErrRuntimeGuard)
111			assert.NotContains(t, err.Error(), dataDir)
112		})
113	}
114}
115
116func TestRuntimeGuard_ReadOnlyLogDirs(t *testing.T) {
117	dataDir := t.TempDir()
118	logs := filepath.Join(dataDir, "logs")
119	require.NoError(t, os.MkdirAll(filepath.Join(logs, "containers"), 0o700))
120	keys := filepath.Join(dataDir, "logs", "keys")
121	secrets := filepath.Join(dataDir, "secrets-run")
122	require.NoError(t, os.MkdirAll(secrets, 0o700))
123	escape := filepath.Join(logs, "escape")
124	require.NoError(t, os.Symlink(secrets, escape))
125	guard := domain.RuntimeGuard{ProtectedPaths: []string{dataDir}, LogDirs: []string{logs}}
126	bind := func(source string, readOnly bool) *domain.ContainerConfig {
127		return guardConfig(domain.ContainerBind{Name: "logs", Source: source, Destination: "/var/log/gordon", ReadOnly: readOnly})
128	}
129
130	require.NoError(t, guard.Check(bind(logs, true)))
131	require.NoError(t, guard.Check(bind(filepath.Join(logs, "containers"), true)))
132
133	withProtected := domain.RuntimeGuard{ProtectedPaths: []string{dataDir, keys}, LogDirs: []string{logs}}
134	refused := map[string]struct {
135		guard domain.RuntimeGuard
136		cfg   *domain.ContainerConfig
137	}{
138		"writable log dir":       {guard, bind(logs, false)},
139		"writable log subdir":    {guard, bind(filepath.Join(logs, "containers"), false)},
140		"data dir read-only":     {guard, bind(dataDir, true)},
141		"sibling of logs":        {guard, bind(filepath.Join(dataDir, "registry"), true)},
142		"symlink out of logs":    {guard, bind(escape, true)},
143		"log dir with protected": {withProtected, bind(logs, true)},
144	}
145	for name, tc := range refused {
146		t.Run(name, func(t *testing.T) {
147			require.ErrorIs(t, tc.guard.Check(tc.cfg), domain.ErrRuntimeGuard)
148		})
149	}
150}

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