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}