gates / gates (push) Successful in 12m26s
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/.
71 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|