package caps // Grants are the OPERATOR half of the capability model (spec §2 "Grants", // §7): a manifest DECLARES `ports` and `host_role`, and nothing materializes // until an operator has granted it ON THIS BOX. The grant lives in the // registry's `caps` column — a place no tenant can write — and this file is // the whole vocabulary of what that column may say. // // The token set is closed and tiny: // // host-role the app's processes may use the box's instance role // port: the app may bind privileged port n (1..1023, never 22/80/443) // doc-engine the app is a document-conversion orchestrator (Chromium, // LibreOffice, Tesseract-class): JIT and resource headroom, // in exchange for a private-network egress fence // // Per route, per box, deliberately (decision 4): `:25` can be held by exactly // one process on one machine, and the IMDS gate is that box's firewall. Two // boxes running the same app is two grants. import ( "fmt" "sort" "strconv" "strings" ) // HostRoleToken is the grant that hands an app the box's cloud instance // role — the one real transfer of authority in the model, and the reason // grants are typed by an operator on the box rather than declared by an app // in a manifest (spec §7). const HostRoleToken = "host-role" // DocEngineToken is the grant that says this route runs a document-conversion // orchestrator — Gotenberg, an OCR service, anything driving Chromium or // LibreOffice over untrusted documents. It is a TRADE, not a gift: the app // gets the resource headroom those engines cannot run without, and in the // same drop-in the platform fences its egress away from every private network // the engine could otherwise be SSRF'd into. RenderService emits both halves // together or neither, so an operator cannot grant the headroom and forget // the fence. // // Not spelled "engine": `carlos store scan` already uses that word for the // antivirus engine throughout its operator-facing prose, and two meanings for // one token would make every caps column ambiguous to read. const DocEngineToken = "doc-engine" // portTokenPrefix is the only other token shape. Kept as a constant so the // parser and the formatter can never drift apart by a character. const portTokenPrefix = "port:" // PortToken is the canonical grant token for a privileged port ("port:25"). // It is the ONLY producer of that string: ValidGrantToken refuses any port // token this function would not have written, so "port:025" and "port:+25" // never reach the column and two spellings of one grant can never coexist. func PortToken(p int) string { return portTokenPrefix + strconv.Itoa(p) } // Grants is a parsed `caps` column: which privileged ports this route may // bind on this box, whether it may use the box's instance role, and whether // it may run as a document-conversion engine. type Grants struct { Ports map[int]bool HostRole bool DocEngine bool } // Has reports whether g holds tok. The token is not validated — an // unparseable token is simply not held, which is the honest answer for // "is this granted?". func (g Grants) Has(tok string) bool { if tok == HostRoleToken { return g.HostRole } if tok == DocEngineToken { return g.DocEngine } if p, ok := parsePortToken(tok); ok { return g.Ports[p] } return false } // String is the canonical column value: every held grant as a token, sorted // lexicographically ("host-role,port:25"), so re-granting a set already // granted writes the same bytes and a diff of two registries is meaningful. // The zero Grants is "". // // The sort is over the token STRINGS, not the port numbers, so ports order // as "port:25", "port:465", "port:5". That is deliberate: the only property // this value needs is that one set has exactly one spelling. func (g Grants) String() string { var toks []string if g.DocEngine { toks = append(toks, DocEngineToken) } if g.HostRole { toks = append(toks, HostRoleToken) } for p, ok := range g.Ports { if ok { toks = append(toks, PortToken(p)) } } sort.Strings(toks) return strings.Join(toks, ",") } // ParseGrants reads the registry's `caps` column. Unknown or malformed // tokens are an ERROR, not a silent skip: this column is written ONLY by // registry.SetCaps behind `carlos route -grant/-revoke`, so anything else in // it means the row was edited by hand, and the honest answer is to refuse to // materialize the route rather than guess which subset was meant. A // duplicate token is refused on the same grounds — String never writes one. // // "" is the zero Grants with no error: nothing granted, which is every row // on every box today and the behaviour byte for byte. func ParseGrants(col string) (Grants, error) { var g Grants if col == "" { return g, nil } for _, tok := range strings.Split(col, ",") { if err := ValidGrantToken(tok); err != nil { return Grants{}, fmt.Errorf("caps column %q: %w", col, err) } if g.Has(tok) { 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) } if tok == HostRoleToken { g.HostRole = true continue } if tok == DocEngineToken { g.DocEngine = true continue } p, _ := parsePortToken(tok) // ValidGrantToken proved the shape if g.Ports == nil { g.Ports = map[int]bool{} } g.Ports[p] = true } return g, nil } // ValidGrantToken is the rail an operator meets at `carlos route -grant`, // and the same rail ParseGrants applies to the stored column. It refuses: // // - an unknown token shape (only "host-role", "doc-engine" and "port:" // exist); // - a non-canonical port spelling ("port:025", "port:+25", "port: 25") — // one grant has exactly one spelling; // - a port outside 1..1023, because a high port needs no help from the // platform and letting the vocabulary cover them would blur what the // capability is for (spec §7); // - 22, 80 and 443, which are reserved ABSOLUTELY and are not grantable: // an operator typo must not be able to hand a tenant the edge's ports or // the box's SSH. // // A token can therefore never contain a comma or a newline, which is what // keeps the comma-separated column unambiguous. func ValidGrantToken(tok string) error { if tok == "" { return fmt.Errorf("grant token is empty — want %q, %q or %q", HostRoleToken, DocEngineToken, PortToken(25)) } if tok == HostRoleToken || tok == DocEngineToken { return nil } if !strings.HasPrefix(tok, portTokenPrefix) { return fmt.Errorf("unknown grant token %q — the vocabulary is %q, %q and %q", tok, HostRoleToken, DocEngineToken, portTokenPrefix+"") } p, ok := parsePortToken(tok) if !ok { return fmt.Errorf("grant token %q: %s must be a decimal port number, canonically spelled (e.g. %q)", tok, portTokenPrefix+"", PortToken(25)) } if p < 1 || p > 1023 { 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) } if reservedPort[p] { 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) } return nil } // parsePortToken returns the port a "port:" token names, and whether the // token is that shape AND spelled the way PortToken would have spelled it. // The round-trip check is what refuses "port:025" and "port:+25": strconv // accepts both, and accepting them would let one grant have two spellings in // one column. func parsePortToken(tok string) (int, bool) { rest, ok := strings.CutPrefix(tok, portTokenPrefix) if !ok { return 0, false } p, err := strconv.Atoi(rest) if err != nil || PortToken(p) != tok { return 0, false } return p, true } // Grant returns the canonical column value with tok added. Granting what is // already granted is idempotent — same set, same bytes — so a re-run of an // operator's command is a success, not a conflict. func Grant(col, tok string) (string, error) { if err := ValidGrantToken(tok); err != nil { return "", err } g, err := ParseGrants(col) if err != nil { return "", err } if tok == HostRoleToken { g.HostRole = true } else if tok == DocEngineToken { g.DocEngine = true } else { p, _ := parsePortToken(tok) if g.Ports == nil { g.Ports = map[int]bool{} } g.Ports[p] = true } return g.String(), nil } // Revoke returns the canonical column value with tok removed. Revoking a // grant that was never made is a quiet success: the caller's goal is "not // granted", not "I removed something" (the same contract DeleteAccount // documents). The token is still validated — an operator who typed a // nonsense token deserves to hear so rather than a cheerful no-op. func Revoke(col, tok string) (string, error) { if err := ValidGrantToken(tok); err != nil { return "", err } g, err := ParseGrants(col) if err != nil { return "", err } if tok == HostRoleToken { g.HostRole = false } else if tok == DocEngineToken { g.DocEngine = false } else if p, ok := parsePortToken(tok); ok { delete(g.Ports, p) } return g.String(), nil }