carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package caps
2
3// The operator half of the model: what may be written into the registry's
4// `caps` column, what a hand-edited column does, and why one granted set has
5// exactly one spelling.
6
7import (
8 "strings"
9 "testing"
10)
11
12// TestValidGrantTokenRefusals pins the closed vocabulary an operator meets at
13// `carlos route -grant`. The three reserved ports are the load-bearing rows:
14// they are refused ABSOLUTELY, so an operator typo can never hand a tenant
15// the edge's ports or the box's SSH (spec §7).
16func TestValidGrantTokenRefusals(t *testing.T) {
17 for _, tok := range []string{HostRoleToken, DocEngineToken, "port:25", "port:465", "port:1", "port:1023"} {
18 if err := ValidGrantToken(tok); err != nil {
19 t.Fatalf("ValidGrantToken(%q) = %v, want accepted", tok, err)
20 }
21 }
22 cases := []struct {
23 tok string
24 says string
25 }{
26 {"port:22", "reserved"},
27 {"port:80", "reserved"},
28 {"port:443", "reserved"},
29 {"port:8080", "1..1023"},
30 {"port:0", "1..1023"},
31 {"port:1024", "1..1023"},
32 {"port:99999", "1..1023"},
33 {"port:-1", "1..1023"},
34 {"hostrole", "unknown grant token"},
35 {"host-role:1", "unknown grant token"},
36 // "engine" alone is NOT the token — it is the word `carlos store scan`
37 // already owns for the antivirus engine, so the caps token is spelled
38 // "doc-engine" to stay unambiguous in a caps column.
39 {"engine", "unknown grant token"},
40 {"docengine", "unknown grant token"},
41 {"", "empty"},
42 {"port:", "decimal port number"},
43 {"port:x", "decimal port number"},
44 // One grant, one spelling: strconv would take all three.
45 {"port:025", "canonically spelled"},
46 {"port:+25", "canonically spelled"},
47 {"port: 25", "decimal port number"},
48 // A token can never carry the column's own separator, or a newline.
49 {"port:25,host-role", "decimal port number"},
50 {"host-role\n", "unknown grant token"},
51 }
52 for _, c := range cases {
53 err := ValidGrantToken(c.tok)
54 if err == nil {
55 t.Fatalf("ValidGrantToken(%q) accepted; want a refusal", c.tok)
56 }
57 if !strings.Contains(err.Error(), c.says) {
58 t.Fatalf("ValidGrantToken(%q) = %q, want it to say %q", c.tok, err, c.says)
59 }
60 }
61}
62
63// TestParseGrantsRoundTrip: the column round-trips, and "" — every row on
64// every box today — is the zero Grants with no error, meaning "nothing
65// granted".
66func TestParseGrantsRoundTrip(t *testing.T) {
67 g, err := ParseGrants("")
68 if err != nil || g.HostRole || len(g.Ports) != 0 {
69 t.Fatalf(`ParseGrants("") = (%+v, %v), want the zero Grants and no error`, g, err)
70 }
71 if g.String() != "" {
72 t.Fatalf("zero Grants formats as %q, want %q", g.String(), "")
73 }
74
75 g, err = ParseGrants("host-role,port:25")
76 if err != nil {
77 t.Fatalf("ParseGrants: %v", err)
78 }
79 if !g.HostRole || !g.Ports[25] || len(g.Ports) != 1 {
80 t.Fatalf("ParseGrants = %+v, want host-role and port 25", g)
81 }
82 if got := g.String(); got != "host-role,port:25" {
83 t.Fatalf("String() = %q, want the canonical column value", got)
84 }
85 // A hand-written column in the other order normalises to the same bytes.
86 g, err = ParseGrants("port:25,host-role")
87 if err != nil {
88 t.Fatalf("ParseGrants: %v", err)
89 }
90 if got := g.String(); got != "host-role,port:25" {
91 t.Fatalf("String() = %q, want the canonical order regardless of input order", got)
92 }
93 if !g.Has(HostRoleToken) || !g.Has("port:25") || g.Has("port:465") || g.Has("nonsense") {
94 t.Fatalf("Has is wrong for %+v", g)
95 }
96}
97
98// TestParseGrantsRefusesAHandEditedColumn: this column is written only by
99// registry.SetCaps behind `carlos route -grant`, so anything the formatter
100// would not have written means a human edited the row — and the honest
101// answer is to refuse to materialize, never to silently honour the subset
102// that happens to parse.
103func TestParseGrantsRefusesAHandEditedColumn(t *testing.T) {
104 for _, col := range []string{
105 "port:99999", // out of range
106 "port:22", // reserved, ungrantable
107 "gpu", // not in the vocabulary
108 "host-role,", // empty element
109 ",port:25", // empty element
110 "host-role, port:25", // whitespace is not trimmed: one spelling only
111 "host-role,host-role", // the formatter never writes a duplicate
112 "port:25,port:025", // nor two spellings of one grant
113 } {
114 if g, err := ParseGrants(col); err == nil {
115 t.Fatalf("ParseGrants(%q) = %+v, want a refusal", col, g)
116 }
117 }
118}
119
120// TestGrantRevokeIdempotent: granting what is granted writes the same bytes,
121// and revoking what was never granted is a quiet success — the caller's goal
122// is "not granted", not "I removed something".
123func TestGrantRevokeIdempotent(t *testing.T) {
124 col, err := Grant("", "port:25")
125 if err != nil || col != "port:25" {
126 t.Fatalf(`Grant("", "port:25") = (%q, %v)`, col, err)
127 }
128 again, err := Grant(col, "port:25")
129 if err != nil || again != col {
130 t.Fatalf("re-granting changed the column: %q -> %q (%v)", col, again, err)
131 }
132 col, err = Grant(col, HostRoleToken)
133 if err != nil || col != "host-role,port:25" {
134 t.Fatalf("Grant(host-role) = (%q, %v), want the canonical two-token column", col, err)
135 }
136 // Order of granting does not change the stored bytes.
137 other, err := Grant("host-role", "port:25")
138 if err != nil || other != col {
139 t.Fatalf("granting in the other order = %q, want %q", other, col)
140 }
141
142 col, err = Revoke(col, "port:25")
143 if err != nil || col != "host-role" {
144 t.Fatalf("Revoke(port:25) = (%q, %v), want just host-role left", col, err)
145 }
146 // Absent grant: no-op, no error.
147 col, err = Revoke(col, "port:25")
148 if err != nil || col != "host-role" {
149 t.Fatalf("revoking an absent grant = (%q, %v), want a quiet no-op", col, err)
150 }
151 col, err = Revoke(col, HostRoleToken)
152 if err != nil || col != "" {
153 t.Fatalf("Revoke(host-role) = (%q, %v), want an empty column", col, err)
154 }
155 // Revoking from an empty column is still a quiet success.
156 if col, err = Revoke("", HostRoleToken); err != nil || col != "" {
157 t.Fatalf(`Revoke("", host-role) = (%q, %v), want ("", nil)`, col, err)
158 }
159 // But a nonsense token is refused rather than cheerfully no-oped, on
160 // both verbs: an operator who typed rubbish deserves to hear so.
161 if _, err := Revoke("port:25", "port:22"); err == nil {
162 t.Fatal("Revoke accepted an ungrantable token")
163 }
164 if _, err := Grant("", "gpu"); err == nil {
165 t.Fatal("Grant accepted a token outside the vocabulary")
166 }
167 // A hand-edited column is refused by both, not repaired by them.
168 if _, err := Grant("gpu", "port:25"); err == nil {
169 t.Fatal("Grant repaired a hand-edited column instead of refusing it")
170 }
171 if _, err := Revoke("gpu", "port:25"); err == nil {
172 t.Fatal("Revoke repaired a hand-edited column instead of refusing it")
173 }
174}
175
176// TestParseGrantsRoundTripsDocEngine pins the new token through the column:
177// parse, Has, String, and the canonical sort order (doc-engine first — "d"
178// sorts before "h" and "p", so a re-grant of the same set writes the same
179// bytes regardless of the order the operator granted them in).
180func TestParseGrantsRoundTripsDocEngine(t *testing.T) {
181 g, err := ParseGrants("doc-engine")
182 if err != nil {
183 t.Fatalf("ParseGrants: %v", err)
184 }
185 if !g.DocEngine || g.HostRole || len(g.Ports) != 0 {
186 t.Fatalf("ParseGrants(doc-engine) = %+v, want only DocEngine set", g)
187 }
188 if !g.Has(DocEngineToken) || g.Has(HostRoleToken) || g.Has("port:25") {
189 t.Fatalf("Has is wrong for %+v", g)
190 }
191 // Canonical order: doc-engine sorts before host-role and any port.
192 g, err = ParseGrants("port:25,host-role,doc-engine")
193 if err != nil {
194 t.Fatalf("ParseGrants: %v", err)
195 }
196 if got := g.String(); got != "doc-engine,host-role,port:25" {
197 t.Fatalf("String() = %q, want doc-engine,host-role,port:25", got)
198 }
199 // A duplicate doc-engine is refused like any other hand-edited duplicate.
200 if _, err := ParseGrants("doc-engine,doc-engine"); err == nil {
201 t.Fatal("ParseGrants accepted a duplicate doc-engine")
202 }
203}
204
205// TestGrantRevokeDocEngine: the new token grants, is idempotent, and revokes,
206// through the same String() canonicalisation the other tokens use.
207func TestGrantRevokeDocEngine(t *testing.T) {
208 col, err := Grant("", DocEngineToken)
209 if err != nil || col != "doc-engine" {
210 t.Fatalf(`Grant("", doc-engine) = (%q, %v)`, col, err)
211 }
212 again, err := Grant(col, DocEngineToken)
213 if err != nil || again != col {
214 t.Fatalf("re-granting doc-engine changed the column: %q -> %q (%v)", col, again, err)
215 }
216 col, err = Grant(col, "port:25")
217 if err != nil || col != "doc-engine,port:25" {
218 t.Fatalf("Grant(port:25) over doc-engine = (%q, %v), want canonical order", col, err)
219 }
220 col, err = Revoke(col, DocEngineToken)
221 if err != nil || col != "port:25" {
222 t.Fatalf("Revoke(doc-engine) = (%q, %v), want just port:25 left", col, err)
223 }
224 // Revoking an absent doc-engine is a quiet success.
225 if col, err = Revoke(col, DocEngineToken); err != nil || col != "port:25" {
226 t.Fatalf("revoking an absent doc-engine = (%q, %v), want a quiet no-op", col, err)
227 }
228}
229
230// TestValidCapabilitiesRefusesHostRoleWithDocEngine pins the contradiction
231// refusal: the two capabilities cannot both be declared, because host_role's
232// IPAddressAllow re-opens the IMDS address that doc_engine's fence exists to
233// close, and in systemd's BPF firewall an allow wins over a deny. The refusal
234// fires on the declaration alone, before any grant is consulted.
235func TestValidCapabilitiesRefusesHostRoleWithDocEngine(t *testing.T) {
236 err := ValidCapabilities("gotenberg", &Set{V: 1, HostRole: true, DocEngine: true})
237 if err == nil {
238 t.Fatal("ValidCapabilities accepted host_role + doc_engine together")
239 }
240 if !strings.Contains(err.Error(), "contradict") {
241 t.Fatalf("refusal %q does not say the two contradict", err)
242 }
243 // Each alone is fine.
244 if err := ValidCapabilities("gotenberg", &Set{V: 1, DocEngine: true}); err != nil {
245 t.Fatalf("doc_engine alone refused: %v", err)
246 }
247 if err := ValidCapabilities("keymail", &Set{V: 1, HostRole: true}); err != nil {
248 t.Fatalf("host_role alone refused: %v", err)
249 }
250}
251
252// TestPortTokenMatchesTheManifestVocabulary: a declared port and its grant
253// token must agree, or a grant an operator types could never satisfy the
254// declaration it was typed for. ValidCapabilities and ValidGrantToken share
255// the reserved list; this pins that they also share the range.
256func TestPortTokenMatchesTheManifestVocabulary(t *testing.T) {
257 for p := 1; p <= 1023; p++ {
258 declarable := validPorts([]Port{{Port: p}}) == nil
259 grantable := ValidGrantToken(PortToken(p)) == nil
260 if declarable != grantable {
261 t.Fatalf("port %d: declarable=%v grantable=%v — a declaration nobody can grant, or a grant nothing can declare",
262 p, declarable, grantable)
263 }
264 }
265}
266