package caps // The operator half of the model: what may be written into the registry's // `caps` column, what a hand-edited column does, and why one granted set has // exactly one spelling. import ( "strings" "testing" ) // TestValidGrantTokenRefusals pins the closed vocabulary an operator meets at // `carlos route -grant`. The three reserved ports are the load-bearing rows: // they are refused ABSOLUTELY, so an operator typo can never hand a tenant // the edge's ports or the box's SSH (spec §7). func TestValidGrantTokenRefusals(t *testing.T) { for _, tok := range []string{HostRoleToken, DocEngineToken, "port:25", "port:465", "port:1", "port:1023"} { if err := ValidGrantToken(tok); err != nil { t.Fatalf("ValidGrantToken(%q) = %v, want accepted", tok, err) } } cases := []struct { tok string says string }{ {"port:22", "reserved"}, {"port:80", "reserved"}, {"port:443", "reserved"}, {"port:8080", "1..1023"}, {"port:0", "1..1023"}, {"port:1024", "1..1023"}, {"port:99999", "1..1023"}, {"port:-1", "1..1023"}, {"hostrole", "unknown grant token"}, {"host-role:1", "unknown grant token"}, // "engine" alone is NOT the token — it is the word `carlos store scan` // already owns for the antivirus engine, so the caps token is spelled // "doc-engine" to stay unambiguous in a caps column. {"engine", "unknown grant token"}, {"docengine", "unknown grant token"}, {"", "empty"}, {"port:", "decimal port number"}, {"port:x", "decimal port number"}, // One grant, one spelling: strconv would take all three. {"port:025", "canonically spelled"}, {"port:+25", "canonically spelled"}, {"port: 25", "decimal port number"}, // A token can never carry the column's own separator, or a newline. {"port:25,host-role", "decimal port number"}, {"host-role\n", "unknown grant token"}, } for _, c := range cases { err := ValidGrantToken(c.tok) if err == nil { t.Fatalf("ValidGrantToken(%q) accepted; want a refusal", c.tok) } if !strings.Contains(err.Error(), c.says) { t.Fatalf("ValidGrantToken(%q) = %q, want it to say %q", c.tok, err, c.says) } } } // TestParseGrantsRoundTrip: the column round-trips, and "" — every row on // every box today — is the zero Grants with no error, meaning "nothing // granted". func TestParseGrantsRoundTrip(t *testing.T) { g, err := ParseGrants("") if err != nil || g.HostRole || len(g.Ports) != 0 { t.Fatalf(`ParseGrants("") = (%+v, %v), want the zero Grants and no error`, g, err) } if g.String() != "" { t.Fatalf("zero Grants formats as %q, want %q", g.String(), "") } g, err = ParseGrants("host-role,port:25") if err != nil { t.Fatalf("ParseGrants: %v", err) } if !g.HostRole || !g.Ports[25] || len(g.Ports) != 1 { t.Fatalf("ParseGrants = %+v, want host-role and port 25", g) } if got := g.String(); got != "host-role,port:25" { t.Fatalf("String() = %q, want the canonical column value", got) } // A hand-written column in the other order normalises to the same bytes. g, err = ParseGrants("port:25,host-role") if err != nil { t.Fatalf("ParseGrants: %v", err) } if got := g.String(); got != "host-role,port:25" { t.Fatalf("String() = %q, want the canonical order regardless of input order", got) } if !g.Has(HostRoleToken) || !g.Has("port:25") || g.Has("port:465") || g.Has("nonsense") { t.Fatalf("Has is wrong for %+v", g) } } // TestParseGrantsRefusesAHandEditedColumn: this column is written only by // registry.SetCaps behind `carlos route -grant`, so anything the formatter // would not have written means a human edited the row — and the honest // answer is to refuse to materialize, never to silently honour the subset // that happens to parse. func TestParseGrantsRefusesAHandEditedColumn(t *testing.T) { for _, col := range []string{ "port:99999", // out of range "port:22", // reserved, ungrantable "gpu", // not in the vocabulary "host-role,", // empty element ",port:25", // empty element "host-role, port:25", // whitespace is not trimmed: one spelling only "host-role,host-role", // the formatter never writes a duplicate "port:25,port:025", // nor two spellings of one grant } { if g, err := ParseGrants(col); err == nil { t.Fatalf("ParseGrants(%q) = %+v, want a refusal", col, g) } } } // TestGrantRevokeIdempotent: granting what is granted writes the same bytes, // and revoking what was never granted is a quiet success — the caller's goal // is "not granted", not "I removed something". func TestGrantRevokeIdempotent(t *testing.T) { col, err := Grant("", "port:25") if err != nil || col != "port:25" { t.Fatalf(`Grant("", "port:25") = (%q, %v)`, col, err) } again, err := Grant(col, "port:25") if err != nil || again != col { t.Fatalf("re-granting changed the column: %q -> %q (%v)", col, again, err) } col, err = Grant(col, HostRoleToken) if err != nil || col != "host-role,port:25" { t.Fatalf("Grant(host-role) = (%q, %v), want the canonical two-token column", col, err) } // Order of granting does not change the stored bytes. other, err := Grant("host-role", "port:25") if err != nil || other != col { t.Fatalf("granting in the other order = %q, want %q", other, col) } col, err = Revoke(col, "port:25") if err != nil || col != "host-role" { t.Fatalf("Revoke(port:25) = (%q, %v), want just host-role left", col, err) } // Absent grant: no-op, no error. col, err = Revoke(col, "port:25") if err != nil || col != "host-role" { t.Fatalf("revoking an absent grant = (%q, %v), want a quiet no-op", col, err) } col, err = Revoke(col, HostRoleToken) if err != nil || col != "" { t.Fatalf("Revoke(host-role) = (%q, %v), want an empty column", col, err) } // Revoking from an empty column is still a quiet success. if col, err = Revoke("", HostRoleToken); err != nil || col != "" { t.Fatalf(`Revoke("", host-role) = (%q, %v), want ("", nil)`, col, err) } // But a nonsense token is refused rather than cheerfully no-oped, on // both verbs: an operator who typed rubbish deserves to hear so. if _, err := Revoke("port:25", "port:22"); err == nil { t.Fatal("Revoke accepted an ungrantable token") } if _, err := Grant("", "gpu"); err == nil { t.Fatal("Grant accepted a token outside the vocabulary") } // A hand-edited column is refused by both, not repaired by them. if _, err := Grant("gpu", "port:25"); err == nil { t.Fatal("Grant repaired a hand-edited column instead of refusing it") } if _, err := Revoke("gpu", "port:25"); err == nil { t.Fatal("Revoke repaired a hand-edited column instead of refusing it") } } // TestParseGrantsRoundTripsDocEngine pins the new token through the column: // parse, Has, String, and the canonical sort order (doc-engine first — "d" // sorts before "h" and "p", so a re-grant of the same set writes the same // bytes regardless of the order the operator granted them in). func TestParseGrantsRoundTripsDocEngine(t *testing.T) { g, err := ParseGrants("doc-engine") if err != nil { t.Fatalf("ParseGrants: %v", err) } if !g.DocEngine || g.HostRole || len(g.Ports) != 0 { t.Fatalf("ParseGrants(doc-engine) = %+v, want only DocEngine set", g) } if !g.Has(DocEngineToken) || g.Has(HostRoleToken) || g.Has("port:25") { t.Fatalf("Has is wrong for %+v", g) } // Canonical order: doc-engine sorts before host-role and any port. g, err = ParseGrants("port:25,host-role,doc-engine") if err != nil { t.Fatalf("ParseGrants: %v", err) } if got := g.String(); got != "doc-engine,host-role,port:25" { t.Fatalf("String() = %q, want doc-engine,host-role,port:25", got) } // A duplicate doc-engine is refused like any other hand-edited duplicate. if _, err := ParseGrants("doc-engine,doc-engine"); err == nil { t.Fatal("ParseGrants accepted a duplicate doc-engine") } } // TestGrantRevokeDocEngine: the new token grants, is idempotent, and revokes, // through the same String() canonicalisation the other tokens use. func TestGrantRevokeDocEngine(t *testing.T) { col, err := Grant("", DocEngineToken) if err != nil || col != "doc-engine" { t.Fatalf(`Grant("", doc-engine) = (%q, %v)`, col, err) } again, err := Grant(col, DocEngineToken) if err != nil || again != col { t.Fatalf("re-granting doc-engine changed the column: %q -> %q (%v)", col, again, err) } col, err = Grant(col, "port:25") if err != nil || col != "doc-engine,port:25" { t.Fatalf("Grant(port:25) over doc-engine = (%q, %v), want canonical order", col, err) } col, err = Revoke(col, DocEngineToken) if err != nil || col != "port:25" { t.Fatalf("Revoke(doc-engine) = (%q, %v), want just port:25 left", col, err) } // Revoking an absent doc-engine is a quiet success. if col, err = Revoke(col, DocEngineToken); err != nil || col != "port:25" { t.Fatalf("revoking an absent doc-engine = (%q, %v), want a quiet no-op", col, err) } } // TestValidCapabilitiesRefusesHostRoleWithDocEngine pins the contradiction // refusal: the two capabilities cannot both be declared, because host_role's // IPAddressAllow re-opens the IMDS address that doc_engine's fence exists to // close, and in systemd's BPF firewall an allow wins over a deny. The refusal // fires on the declaration alone, before any grant is consulted. func TestValidCapabilitiesRefusesHostRoleWithDocEngine(t *testing.T) { err := ValidCapabilities("gotenberg", &Set{V: 1, HostRole: true, DocEngine: true}) if err == nil { t.Fatal("ValidCapabilities accepted host_role + doc_engine together") } if !strings.Contains(err.Error(), "contradict") { t.Fatalf("refusal %q does not say the two contradict", err) } // Each alone is fine. if err := ValidCapabilities("gotenberg", &Set{V: 1, DocEngine: true}); err != nil { t.Fatalf("doc_engine alone refused: %v", err) } if err := ValidCapabilities("keymail", &Set{V: 1, HostRole: true}); err != nil { t.Fatalf("host_role alone refused: %v", err) } } // TestPortTokenMatchesTheManifestVocabulary: a declared port and its grant // token must agree, or a grant an operator types could never satisfy the // declaration it was typed for. ValidCapabilities and ValidGrantToken share // the reserved list; this pins that they also share the range. func TestPortTokenMatchesTheManifestVocabulary(t *testing.T) { for p := 1; p <= 1023; p++ { declarable := validPorts([]Port{{Port: p}}) == nil grantable := ValidGrantToken(PortToken(p)) == nil if declarable != grantable { t.Fatalf("port %d: declarable=%v grantable=%v — a declaration nobody can grant, or a grant nothing can declare", p, declarable, grantable) } } }