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// Grants are the OPERATOR half of the capability model (spec §2 "Grants",
4// §7): a manifest DECLARES `ports` and `host_role`, and nothing materializes
5// until an operator has granted it ON THIS BOX. The grant lives in the
6// registry's `caps` column — a place no tenant can write — and this file is
7// the whole vocabulary of what that column may say.
8//
9// The token set is closed and tiny:
10//
11// host-role the app's processes may use the box's instance role
12// port:<n> the app may bind privileged port n (1..1023, never 22/80/443)
13// doc-engine the app is a document-conversion orchestrator (Chromium,
14// LibreOffice, Tesseract-class): JIT and resource headroom,
15// in exchange for a private-network egress fence
16//
17// Per route, per box, deliberately (decision 4): `:25` can be held by exactly
18// one process on one machine, and the IMDS gate is that box's firewall. Two
19// boxes running the same app is two grants.
20
21import (
22 "fmt"
23 "sort"
24 "strconv"
25 "strings"
26)
27
28// HostRoleToken is the grant that hands an app the box's cloud instance
29// role — the one real transfer of authority in the model, and the reason
30// grants are typed by an operator on the box rather than declared by an app
31// in a manifest (spec §7).
32const HostRoleToken = "host-role"
33
34// DocEngineToken is the grant that says this route runs a document-conversion
35// orchestrator — Gotenberg, an OCR service, anything driving Chromium or
36// LibreOffice over untrusted documents. It is a TRADE, not a gift: the app
37// gets the resource headroom those engines cannot run without, and in the
38// same drop-in the platform fences its egress away from every private network
39// the engine could otherwise be SSRF'd into. RenderService emits both halves
40// together or neither, so an operator cannot grant the headroom and forget
41// the fence.
42//
43// Not spelled "engine": `carlos store scan` already uses that word for the
44// antivirus engine throughout its operator-facing prose, and two meanings for
45// one token would make every caps column ambiguous to read.
46const DocEngineToken = "doc-engine"
47
48// portTokenPrefix is the only other token shape. Kept as a constant so the
49// parser and the formatter can never drift apart by a character.
50const portTokenPrefix = "port:"
51
52// PortToken is the canonical grant token for a privileged port ("port:25").
53// It is the ONLY producer of that string: ValidGrantToken refuses any port
54// token this function would not have written, so "port:025" and "port:+25"
55// never reach the column and two spellings of one grant can never coexist.
56func PortToken(p int) string { return portTokenPrefix + strconv.Itoa(p) }
57
58// Grants is a parsed `caps` column: which privileged ports this route may
59// bind on this box, whether it may use the box's instance role, and whether
60// it may run as a document-conversion engine.
61type Grants struct {
62 Ports map[int]bool
63 HostRole bool
64 DocEngine bool
65}
66
67// Has reports whether g holds tok. The token is not validated — an
68// unparseable token is simply not held, which is the honest answer for
69// "is this granted?".
70func (g Grants) Has(tok string) bool {
71 if tok == HostRoleToken {
72 return g.HostRole
73 }
74 if tok == DocEngineToken {
75 return g.DocEngine
76 }
77 if p, ok := parsePortToken(tok); ok {
78 return g.Ports[p]
79 }
80 return false
81}
82
83// String is the canonical column value: every held grant as a token, sorted
84// lexicographically ("host-role,port:25"), so re-granting a set already
85// granted writes the same bytes and a diff of two registries is meaningful.
86// The zero Grants is "".
87//
88// The sort is over the token STRINGS, not the port numbers, so ports order
89// as "port:25", "port:465", "port:5". That is deliberate: the only property
90// this value needs is that one set has exactly one spelling.
91func (g Grants) String() string {
92 var toks []string
93 if g.DocEngine {
94 toks = append(toks, DocEngineToken)
95 }
96 if g.HostRole {
97 toks = append(toks, HostRoleToken)
98 }
99 for p, ok := range g.Ports {
100 if ok {
101 toks = append(toks, PortToken(p))
102 }
103 }
104 sort.Strings(toks)
105 return strings.Join(toks, ",")
106}
107
108// ParseGrants reads the registry's `caps` column. Unknown or malformed
109// tokens are an ERROR, not a silent skip: this column is written ONLY by
110// registry.SetCaps behind `carlos route -grant/-revoke`, so anything else in
111// it means the row was edited by hand, and the honest answer is to refuse to
112// materialize the route rather than guess which subset was meant. A
113// duplicate token is refused on the same grounds — String never writes one.
114//
115// "" is the zero Grants with no error: nothing granted, which is every row
116// on every box today and the behaviour byte for byte.
117func ParseGrants(col string) (Grants, error) {
118 var g Grants
119 if col == "" {
120 return g, nil
121 }
122 for _, tok := range strings.Split(col, ",") {
123 if err := ValidGrantToken(tok); err != nil {
124 return Grants{}, fmt.Errorf("caps column %q: %w", col, err)
125 }
126 if g.Has(tok) {
127 return Grants{}, fmt.Errorf("caps column %q: %q appears more than once — this column is written only by `carlos route --grant`, so a duplicate means a hand edit", col, tok)
128 }
129 if tok == HostRoleToken {
130 g.HostRole = true
131 continue
132 }
133 if tok == DocEngineToken {
134 g.DocEngine = true
135 continue
136 }
137 p, _ := parsePortToken(tok) // ValidGrantToken proved the shape
138 if g.Ports == nil {
139 g.Ports = map[int]bool{}
140 }
141 g.Ports[p] = true
142 }
143 return g, nil
144}
145
146// ValidGrantToken is the rail an operator meets at `carlos route -grant`,
147// and the same rail ParseGrants applies to the stored column. It refuses:
148//
149// - an unknown token shape (only "host-role", "doc-engine" and "port:<n>"
150// exist);
151// - a non-canonical port spelling ("port:025", "port:+25", "port: 25") —
152// one grant has exactly one spelling;
153// - a port outside 1..1023, because a high port needs no help from the
154// platform and letting the vocabulary cover them would blur what the
155// capability is for (spec §7);
156// - 22, 80 and 443, which are reserved ABSOLUTELY and are not grantable:
157// an operator typo must not be able to hand a tenant the edge's ports or
158// the box's SSH.
159//
160// A token can therefore never contain a comma or a newline, which is what
161// keeps the comma-separated column unambiguous.
162func ValidGrantToken(tok string) error {
163 if tok == "" {
164 return fmt.Errorf("grant token is empty — want %q, %q or %q", HostRoleToken, DocEngineToken, PortToken(25))
165 }
166 if tok == HostRoleToken || tok == DocEngineToken {
167 return nil
168 }
169 if !strings.HasPrefix(tok, portTokenPrefix) {
170 return fmt.Errorf("unknown grant token %q — the vocabulary is %q, %q and %q", tok, HostRoleToken, DocEngineToken, portTokenPrefix+"<n>")
171 }
172 p, ok := parsePortToken(tok)
173 if !ok {
174 return fmt.Errorf("grant token %q: %s must be a decimal port number, canonically spelled (e.g. %q)", tok, portTokenPrefix+"<n>", PortToken(25))
175 }
176 if p < 1 || p > 1023 {
177 return fmt.Errorf("grant token %q: port %d is outside 1..1023 — only privileged ports are a capability; a high port needs no help from the platform", tok, p)
178 }
179 if reservedPort[p] {
180 return fmt.Errorf("grant token %q: port %d is reserved absolutely and is not grantable — it belongs to the edge or to SSH", tok, p)
181 }
182 return nil
183}
184
185// parsePortToken returns the port a "port:<n>" token names, and whether the
186// token is that shape AND spelled the way PortToken would have spelled it.
187// The round-trip check is what refuses "port:025" and "port:+25": strconv
188// accepts both, and accepting them would let one grant have two spellings in
189// one column.
190func parsePortToken(tok string) (int, bool) {
191 rest, ok := strings.CutPrefix(tok, portTokenPrefix)
192 if !ok {
193 return 0, false
194 }
195 p, err := strconv.Atoi(rest)
196 if err != nil || PortToken(p) != tok {
197 return 0, false
198 }
199 return p, true
200}
201
202// Grant returns the canonical column value with tok added. Granting what is
203// already granted is idempotent — same set, same bytes — so a re-run of an
204// operator's command is a success, not a conflict.
205func Grant(col, tok string) (string, error) {
206 if err := ValidGrantToken(tok); err != nil {
207 return "", err
208 }
209 g, err := ParseGrants(col)
210 if err != nil {
211 return "", err
212 }
213 if tok == HostRoleToken {
214 g.HostRole = true
215 } else if tok == DocEngineToken {
216 g.DocEngine = true
217 } else {
218 p, _ := parsePortToken(tok)
219 if g.Ports == nil {
220 g.Ports = map[int]bool{}
221 }
222 g.Ports[p] = true
223 }
224 return g.String(), nil
225}
226
227// Revoke returns the canonical column value with tok removed. Revoking a
228// grant that was never made is a quiet success: the caller's goal is "not
229// granted", not "I removed something" (the same contract DeleteAccount
230// documents). The token is still validated — an operator who typed a
231// nonsense token deserves to hear so rather than a cheerful no-op.
232func Revoke(col, tok string) (string, error) {
233 if err := ValidGrantToken(tok); err != nil {
234 return "", err
235 }
236 g, err := ParseGrants(col)
237 if err != nil {
238 return "", err
239 }
240 if tok == HostRoleToken {
241 g.HostRole = false
242 } else if tok == DocEngineToken {
243 g.DocEngine = false
244 } else if p, ok := parsePortToken(tok); ok {
245 delete(g.Ports, p)
246 }
247 return g.String(), nil
248}
249