Files
felhom.eu/hub/internal/web/r975_form_tokens_test.go
T
admin 5277d859d5
gates / gates (push) Successful in 12m26s
hub 0.168.0: seal the last plain tokens in config_json (R-975, R-1241's Felhom half); decide row R-1243 for the hub's plain Longhorn copies (register 188 -> 189)
The Cloudflare tunnel token, Cloudflare API token and git token inside customer_configs.config_json now hold the
05 §16.2 seal (enc:v1:, OFFSITE_SECRET_KEY). Old rows sealed once at start-up without a config_version bump, then
VACUUM + WAL checkpoint (an UPDATE leaves the old plaintext in a freed block). -unseal-box-secrets opens them too.
Eight tests, each red-proved. Built, not deployed. Audit: documentation/audits/hub-token-seal-2026-10-23/.
2026-10-23 19:48:17 +02:00

71 lines
2.3 KiB
Go

package web
import (
"encoding/json"
"io"
"log"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-975: every secret the operator's edit form writes into config_json is stored sealed. The form is
// posted with EVERY field buildConfigJSON reads; each leaf of the stored JSON must then be either on the
// short non-secret list below or sealed. A new secret field added to the form without adding its path
// to store.configTokenPaths fails here (its leaf is neither).
func TestR975_EveryFormSecretIsSealed(t *testing.T) {
st, err := store.New(filepath.Join(t.TempDir(), "t.db"), log.New(io.Discard, "", 0))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
form := "cf_tunnel_token=tunnel-canary-r975&cf_api_token=cfapi-canary-r975&git_username=bot&git_token=git-canary-r975&debug_mode=on"
js := buildConfigJSON(postForm("/configs/c1/edit", form))
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", RetrievalPassword: "p", APIKey: "k", ConfigJSON: js}); err != nil {
t.Fatal(err)
}
raw, err := st.RawConfigJSONForTest("c1")
if err != nil {
t.Fatal(err)
}
for _, c := range []string{"tunnel-canary-r975", "cfapi-canary-r975", "git-canary-r975"} {
if strings.Contains(raw, c) {
t.Errorf("stored config_json holds the form's secret %q in plaintext", c)
}
}
notSecret := map[string]bool{"git.username": true, "logging.level": true}
var m map[string]any
if err := json.Unmarshal([]byte(raw), &m); err != nil {
t.Fatal(err)
}
leaves := 0
var walk func(p string, v any)
walk = func(p string, v any) {
switch x := v.(type) {
case map[string]any:
for k, vv := range x {
walk(strings.TrimPrefix(p+"."+k, "."), vv)
}
case string:
leaves++
if !notSecret[p] && !strings.HasPrefix(x, "enc:v1:") {
t.Errorf("config_json leaf %s from the form is neither on the non-secret list nor sealed", p)
}
}
}
walk("", m)
if leaves != 5 {
t.Fatalf("the form wrote %d leaves, want 5 — a field was added or lost; classify it here", leaves)
}
// And the hub reads the plaintext back (storedCFToken feeds the Cloudflare zone check).
cfg, err := st.GetCustomerConfig("c1")
if err != nil || cfg == nil {
t.Fatal(err)
}
if got := storedCFToken(cfg.ConfigJSON); got != "cfapi-canary-r975" {
t.Fatalf("storedCFToken after the round trip = %q", got)
}
}