main bac21c6

app_capability_policy_test.go RawHistoryFolder

1package domain
2
3import (
4	"errors"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10)
11
12func validCapabilityPolicy() AppCapabilityPolicy {
13	return AppCapabilityPolicy{
14		Name:            "net-admin",
15		Add:             []string{"NET_ADMIN"},
16		AllowedApps:     []string{"demo"},
17		AllowedServices: []string{"worker"},
18	}
19}
20
21func TestCapabilityPolicyValid(t *testing.T) {
22	p := validCapabilityPolicy()
23	require.NoError(t, p.Validate())
24	p.Add = []string{"NET_ADMIN", "IPC_LOCK", "SYS_NICE"}
25	require.NoError(t, p.Validate())
26}
27
28func TestCapabilityPolicyNameRejected(t *testing.T) {
29	for _, name := range []string{"", "Bad", "a--b", "has space"} {
30		p := validCapabilityPolicy()
31		p.Name = name
32		err := p.Validate()
33		require.Error(t, err, "name %q", name)
34		assert.ErrorIs(t, err, ErrCapabilityPolicy)
35	}
36}
37
38func TestCapabilityPolicyAddRejected(t *testing.T) {
39	cases := map[string][]string{
40		"empty":       {},
41		"empty entry": {""},
42		"ALL":         {"ALL"},
43		"SYS_ADMIN":   {"SYS_ADMIN"},
44		"SYS_PTRACE":  {"SYS_PTRACE"},
45		"SYS_MODULE":  {"SYS_MODULE"},
46		"BPF":         {"BPF"},
47		"lowercase":   {"net_admin"},
48		"CAP_ prefix": {"CAP_NET_ADMIN"},
49		"unknown":     {"NOT_A_CAP"},
50		"duplicate":   {"NET_ADMIN", "NET_ADMIN"},
51		"mixed":       {"NET_ADMIN", "SYS_ADMIN"},
52	}
53	for name, add := range cases {
54		p := validCapabilityPolicy()
55		p.Add = add
56		err := p.Validate()
57		require.Error(t, err, "case %s", name)
58		assert.ErrorIs(t, err, ErrCapabilityPolicy, "case %s", name)
59	}
60}
61
62func TestCapabilityPolicyAllowlistRejected(t *testing.T) {
63	p := validCapabilityPolicy()
64	p.AllowedApps = nil
65	require.ErrorIs(t, p.Validate(), ErrCapabilityPolicy)
66
67	p = validCapabilityPolicy()
68	p.AllowedServices = nil
69	require.ErrorIs(t, p.Validate(), ErrCapabilityPolicy)
70
71	p = validCapabilityPolicy()
72	p.AllowedApps = []string{"demo", "demo"}
73	require.Error(t, p.Validate())
74
75	p = validCapabilityPolicy()
76	p.AllowedServices = []string{"*"}
77	require.Error(t, p.Validate())
78}
79
80func TestAllowedAppCapabilitiesExcludesDangerous(t *testing.T) {
81	allowed := AllowedAppCapabilities()
82	for _, bad := range []string{"ALL", "SYS_ADMIN", "SYS_PTRACE", "SYS_MODULE", "SYS_RAWIO", "SYS_BOOT", "SYS_TIME", "DAC_READ_SEARCH", "BPF", "PERFMON", "MAC_ADMIN", "MAC_OVERRIDE", "AUDIT_CONTROL", "SYSLOG"} {
83		assert.NotContains(t, allowed, bad)
84	}
85	assert.Contains(t, allowed, "NET_ADMIN")
86	// The returned slice is a copy.
87	allowed[0] = "mutated"
88	assert.NotEqual(t, "mutated", AllowedAppCapabilities()[0])
89}
90
91func TestResolveAppCapabilitiesDenyByDefault(t *testing.T) {
92	p := validCapabilityPolicy()
93	_, err := p.ResolveAppCapabilities("other", "worker")
94	require.ErrorIs(t, err, ErrCapabilityPolicy)
95	_, err = p.ResolveAppCapabilities("demo", "helper")
96	require.ErrorIs(t, err, ErrCapabilityPolicy)
97}
98
99func TestResolveAppCapabilitiesExactGrantIsCopy(t *testing.T) {
100	p := validCapabilityPolicy()
101	caps, err := p.ResolveAppCapabilities("demo", "worker")
102	require.NoError(t, err)
103	assert.Equal(t, []string{"NET_ADMIN"}, caps)
104	caps[0] = "mutated"
105	again, err := p.ResolveAppCapabilities("demo", "worker")
106	require.NoError(t, err)
107	assert.Equal(t, []string{"NET_ADMIN"}, again)
108}
109
110func TestResolveAppCapabilitiesUnionSortedDeduped(t *testing.T) {
111	policies := map[string]AppCapabilityPolicy{
112		"a": {Name: "a", Add: []string{"NET_ADMIN", "IPC_LOCK"}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
113		"b": {Name: "b", Add: []string{"NET_ADMIN", "SYS_NICE"}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
114	}
115	caps, err := ResolveAppCapabilities("demo", "worker", []string{"b", "a"}, policies)
116	require.NoError(t, err)
117	assert.Equal(t, []string{"IPC_LOCK", "NET_ADMIN", "SYS_NICE"}, caps)
118}
119
120func TestResolveAppCapabilitiesUnknownAndEmpty(t *testing.T) {
121	_, err := ResolveAppCapabilities("demo", "worker", []string{"unknown"}, nil)
122	require.ErrorIs(t, err, ErrCapabilityPolicy)
123	caps, err := ResolveAppCapabilities("demo", "worker", nil, nil)
124	require.NoError(t, err)
125	assert.Nil(t, caps)
126}
127
128func TestResolveAppCapabilitiesRefusedDoesNotEchoCaps(t *testing.T) {
129	policies := map[string]AppCapabilityPolicy{"net-admin": validCapabilityPolicy()}
130	_, err := ResolveAppCapabilities("other", "worker", []string{"net-admin"}, policies)
131	require.ErrorIs(t, err, ErrCapabilityPolicy)
132	assert.NotContains(t, err.Error(), "NET_ADMIN")
133}
134
135func TestRuntimeGuardRefusesDisallowedAddedCapabilities(t *testing.T) {
136	var g RuntimeGuard
137	require.NoError(t, g.Check(&ContainerConfig{AddedCapabilities: []string{"NET_ADMIN"}}))
138	for _, caps := range [][]string{{"ALL"}, {"SYS_ADMIN"}, {"NET_ADMIN", "NET_ADMIN"}} {
139		err := g.Check(&ContainerConfig{AddedCapabilities: caps})
140		require.ErrorIs(t, err, ErrRuntimeGuard, "caps %v", caps)
141	}
142}
143
144func TestAppServiceCapabilitiesValidation(t *testing.T) {
145	mk := func(caps ...string) AppSpec {
146		return AppSpec{Name: "demo", Services: []AppService{{Name: "worker", Image: "registry.example.com/demo/worker:1", StopGrace: 10 * time.Second, Readiness: AppReadiness{Type: "http", Path: "/healthz", Timeout: 30 * time.Second}, Capabilities: caps}}}
147	}
148	require.NoError(t, mk("net-admin").Validate())
149
150	err := mk("net-admin", "net-admin").Validate()
151	require.ErrorIs(t, err, ErrInvalidAppSpec)
152	assert.Contains(t, err.Error(), "duplicate capability")
153
154	for _, name := range []string{"Bad!", "NET_ADMIN", "a--b", ""} {
155		err = mk(name).Validate()
156		require.Error(t, err, "name %q", name)
157		assert.True(t, errors.Is(err, ErrInvalidAppSpec))
158	}
159}
160
161func TestDiffAppSpecCapabilities(t *testing.T) {
162	svc := func(caps ...string) AppService {
163		return AppService{Name: "worker", Image: "registry.example.com/demo/worker:1", StopGrace: 10 * time.Second, Readiness: AppReadiness{Type: "http", Path: "/healthz", Timeout: 30 * time.Second}, Capabilities: caps}
164	}
165	mk := func(s AppService) AppSpec { return AppSpec{Name: "demo", Services: []AppService{s}} }
166
167	diff := DiffAppSpec(mk(svc()), mk(svc("net-admin")))
168	assert.Contains(t, diff.Changed, "service/worker/capabilities")
169
170	diff = DiffAppSpec(mk(svc("net-admin")), mk(svc()))
171	assert.Contains(t, diff.Changed, "service/worker/capabilities")
172
173	diff = DiffAppSpec(mk(svc("a", "b")), mk(svc("b", "a")))
174	assert.NotContains(t, diff.Changed, "service/worker/capabilities", "reorder is a no-op")
175
176	diff = DiffAppSpec(mk(svc()), mk(svc([]string{}...)))
177	assert.NotContains(t, diff.Changed, "service/worker/capabilities", "nil vs empty is a no-op")
178	assert.NotContains(t, diff.Changed, "service/worker/devices")
179}
180
181func TestExcessCapabilities(t *testing.T) {
182	authorized := append(DefaultAppCapAdd(), "IPC_LOCK")
183	assert.Empty(t, ExcessCapabilities([]string{"CAP_CHOWN", "ipc_lock"}, authorized))
184	assert.Empty(t, ExcessCapabilities(nil, authorized))
185	assert.Equal(t, []string{"NET_ADMIN"}, ExcessCapabilities([]string{"CHOWN", "CAP_NET_ADMIN", "NET_ADMIN"}, authorized))
186	assert.Equal(t, "NET_ADMIN", NormalizeCapability(" cap_net_admin "))
187}
188
189func TestDefaultAppCapAddIsCopy(t *testing.T) {
190	d := DefaultAppCapAdd()
191	d[0] = "mutated"
192	assert.NotEqual(t, "mutated", DefaultAppCapAdd()[0])
193}

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