Files
privatae-bastion/client-go/freeagent_managed_test.go
T
Privatae 1176f7fd0b
ci / personal-data (push) Successful in 8s
ci / test (push) Successful in 7m17s
ci / fuzz (push) Successful in 6m33s
bastion 0.61.0
2026-11-02 06:20:42 +00:00

664 lines
25 KiB
Go

//go:build !windows
// Tagged !windows. It always WAS unix-only — it calls faTestUser,
// which lives in freeagent_drop_test.go behind `//go:build !windows` — so this
// file has never compiled for Windows and `GOOS=windows go vet ./...` has been
// red on it the whole time. The tag does not remove any coverage; it states the
// dependency that was already there and lets vet run clean on the target, which
// is what makes a REAL Windows regression visible instead of being lost in a
// failure everyone has learned to scroll past.
package main
// The managed lane, proven on this box: the artifact fetch with hash
// verification, the identity, the manifest rendered from a grant, and —
// when a real freeagent .pyz and python3 are available (FREEAGENT_TEST_PYZ)
// — the whole thing end to end: a signed grant with 'search' brings up a
// local agent that answers /health and /catalog over mTLS to this box.
import (
"crypto/ed25519"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"net/http/httptest"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
)
func stubPublish(t *testing.T, pyz []byte, sha string) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
var srv *httptest.Server
mux.HandleFunc("/bastion-agent/version.json", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"version": "9.9.9", "files": map[string]any{
"bastion-agent-9.9.9.pyz": map[string]any{"url": srv.URL + "/bastion-agent/dl/bastion-agent-9.9.9.pyz", "sha256": sha}}})
})
mux.HandleFunc("/bastion-agent/dl/bastion-agent-9.9.9.pyz", func(w http.ResponseWriter, r *http.Request) { w.Write(pyz) })
srv = httptest.NewServer(signedStub(releaseKindAgent, mux))
t.Cleanup(srv.Close)
return srv
}
func withConfigDir(t *testing.T) string {
t.Helper()
d := t.TempDir()
switch {
case os.Getenv("XDG_CONFIG_HOME") != "" || true:
t.Setenv("XDG_CONFIG_HOME", d) // UserConfigDir on linux
t.Setenv("HOME", d)
t.Setenv("AppData", d)
}
// The environment alone does NOT isolate faBaseDir. On a ROOT install it
// prefers faStateBase() — "/var/lib/privatae/freeagent"
// (freeagent_drop.go:318-326) — which no environment variable reaches. So
// `go test` as root wrote a fake runtime into the LIVE bastion's state, and
// a later case could return that cached copy instead of exercising the code
// under test. Measured: a run left a 27-byte freeagent.pyz and version
// 2.2.2 under that base, on a machine with an active bastion. Isolation
// is the override the drop tests already use.
// The machine marker is machine-scoped, and claim-anchor now consults it
// (singleton.go). Without this, an anchor test on a box that HAS a marker
// gets the singleton refusal instead of the refusal it is asserting — which
// is exactly what happened.
oldMarker := markerPathOverride
markerPathOverride = filepath.Join(d, "machine.json")
t.Cleanup(func() { markerPathOverride = oldMarker })
oldBase := faStateBaseOverride
faStateBaseOverride = filepath.Join(d, "state")
if err := os.MkdirAll(faStateBaseOverride, 0o755); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { faStateBaseOverride = oldBase })
return d
}
func TestFaEnsurePyzVerifiesHash(t *testing.T) {
withConfigDir(t)
body := []byte("#!/usr/bin/env python3\nfake")
sum := sha256.Sum256(body)
good := hex.EncodeToString(sum[:])
srv := stubPublish(t, body, good)
old := faManifestURL
faManifestURL = srv.URL + "/bastion-agent/version.json"
t.Cleanup(func() { faManifestURL = old })
p, v, err := faEnsurePyz()
if err != nil || v != "9.9.9" {
t.Fatalf("good hash must install: %v %q", err, v)
}
got, _ := os.ReadFile(p)
if string(got) != string(body) {
t.Fatal("installed bytes differ")
}
// same version again: no re-download needed (still returns the copy)
if _, v2, err := faEnsurePyz(); err != nil || v2 != "9.9.9" {
t.Fatalf("cached copy: %v", err)
}
// a tampered file is refused and the old copy stays
bad := stubPublish(t, append(body, 'x'), good)
faManifestURL = bad.URL + "/bastion-agent/version.json"
_ = os.Remove(filepath.Join(filepath.Dir(p), "freeagent.version"))
if _, _, err := faEnsurePyz(); err == nil || !strings.Contains(err.Error(), "sha256") {
t.Fatalf("hash mismatch must refuse, got %v", err)
}
// Refused means the installed agent is the one that was verified, byte for
// byte: a refusal after the tampered bytes were written would still run them.
if got, err := os.ReadFile(p); err != nil || string(got) != string(body) {
t.Fatalf("after a refused download the installed agent must be unchanged: %v %q", err, got)
}
// offline with an installed copy: still served
faManifestURL = "http://127.0.0.1:9/nope"
_ = atomicWrite(filepath.Join(filepath.Dir(p), "freeagent.version"), []byte("9.9.9\n"))
if p2, v3, err := faEnsurePyz(); err != nil || p2 != p || v3 != "9.9.9" {
t.Fatalf("offline must fall back to the installed copy: %v", err)
}
}
func TestFaEnsureIdentity(t *testing.T) {
dir := t.TempDir()
cert, key, _, err := faEnsureIdentity(dir, "store-7")
if err != nil {
t.Fatal(err)
}
raw, _ := os.ReadFile(cert)
blk, _ := pem.Decode(raw)
c, err := x509.ParseCertificate(blk.Bytes)
if err != nil || c.Subject.CommonName != "store-7" || !c.IsCA {
t.Fatalf("cert: %v cn=%q ca=%v", err, c.Subject.CommonName, c.IsCA)
}
pool := x509.NewCertPool()
pool.AddCert(c)
if _, err := c.Verify(x509.VerifyOptions{Roots: pool}); err != nil {
t.Fatalf("must be its own root: %v", err)
}
st, _ := os.Stat(key)
if st.Mode().Perm()&0o077 != 0 {
t.Fatalf("key must be private, mode %v", st.Mode())
}
// idempotent: same files on the second call
c2, k2, _, _ := faEnsureIdentity(dir, "store-7")
if c2 != cert || k2 != key {
t.Fatal("identity must be minted once")
}
}
func TestFaManifestText(t *testing.T) {
terms := &dcnTerms{Class: "machine", Root: `C:\Users\user\Documents\context`,
Mask: []string{"read", "search", "index"}, Excludes: []string{".ssh", "*.key"}}
m := faManifestText("store-1", "store-1", terms, "daily", "age1abc", "qwen3-embedding:4b", 2560)
for _, want := range []string{
`roots: ["C:\\Users\\user\\Documents\\context"]`, `exclude: [".ssh", "*.key"]`,
"mask: [read, search, index]", "stages: [walk, extract, sanitize, chunk, embed, catalog, seal]",
"sweep: daily", "catalog: host", "plaintext: never", "dim: 2560", `model: "qwen3-embedding:4b"`} {
if !strings.Contains(m, want) {
t.Fatalf("manifest missing %q:\n%s", want, m)
}
}
// no embedding model on the box → no chunk/embed; no recipient → no seal, local catalog
m = faManifestText("store-2", "store-2", terms, "", "", "", 0)
if !strings.Contains(m, "stages: [walk, extract, sanitize, catalog]") || !strings.Contains(m, "catalog: local") ||
!strings.Contains(m, "sweep: hourly") {
t.Fatalf("degraded manifest wrong:\n%s", m)
}
// read-only grant: no index → catalog needs index; only walk/extract/sanitize
ro := &dcnTerms{Root: "/data", Mask: []string{"read"}}
m = faManifestText("s", "s", ro, "15m", "", "m", 4)
if !strings.Contains(m, "stages: [walk, extract, sanitize]") || !strings.Contains(m, "mask: [read]") {
t.Fatalf("read-only manifest wrong:\n%s", m)
}
}
func TestFaManagedConfigFromPortFile(t *testing.T) {
dir := t.TempDir()
faMgr.Lock()
faMgr.inst[faKey{"dcn-test", 4242}] = &faInstance{StoreID: 4242, Dir: dir, Cert: "c", Key: "k"}
faMgr.Unlock()
t.Cleanup(func() { faStop("dcn-test", 4242) })
if _, ok := faManagedConfig("dcn-test", 4242); ok {
t.Fatal("no port file → not configured")
}
os.WriteFile(filepath.Join(dir, "port"), []byte("18473\n"), 0o600)
c, ok := faManagedConfig("dcn-test", 4242)
if !ok || c.Addr != "127.0.0.1:18473" || c.CA != "c" {
t.Fatalf("port file → config, got %+v %v", c, ok)
}
if _, ok := faConfigFor("dcn-test", 4242); !ok {
t.Fatal("faConfigFor must prefer the managed instance")
}
t.Setenv("PRIVATAE_BASTION_AGENT_ADDR", "")
if _, ok := faConfigFor("dcn-test", 1); ok {
t.Fatal("unknown store with no env → absent")
}
if !strings.Contains(faAbsentReason("dcn-test", 1), "no local agent") {
t.Fatal("absent reason for an unknown store")
}
}
// A beat's freeagent capability and freeagent_ok are its own pairing's agents
// only. Probed across every pairing, Host B's beat carried Host A's agent's
// health, and with B's freeagent_stores now empty the Host reads the
// capability alone (the Host's store-card check: `"freeagent"
// in caps and not stores`), so A's serving agent marked every one of B's
// stores on this machine as served.
func TestHeartbeatProbesOnlyItsOwnPairingsAgents(t *testing.T) {
t.Setenv("PRIVATAE_BASTION_AGENT_ADDR", "") // no hand-run agent
dcnA, dcnB := t.TempDir(), t.TempDir()
faMgr.Lock()
// no port file: A's agent is managed and not serving
faMgr.inst[faKey{dcnA, 7}] = &faInstance{StoreID: 7, Dir: t.TempDir(), Cert: "a", Key: "a"}
faMgr.Unlock()
t.Cleanup(func() { faStop(dcnA, 7) })
beat := func(dir string) map[string]any {
var body map[string]any
raw := buildHeartbeatBodyOn(beatLanes{dcnDir: dir}, "", "box", "linux", "test", "pub", "fp", "", "", false, nil, nil, nil)
if err := json.Unmarshal([]byte(raw), &body); err != nil {
t.Fatal(err)
}
return body
}
// MUTATION: probe every pairing's instances (no k.Dir filter in
// faManagedProbe). RESULT: red, B's beat carries freeagent_ok false.
if b, ok := beat(dcnB)["freeagent_ok"]; ok {
t.Errorf("Host B holds no agent here, yet its beat says freeagent_ok %v", b)
}
if b, ok := beat(dcnA)["freeagent_ok"]; !ok || b != false {
t.Errorf("Host A's beat must carry its own agent's health: %v %v", b, ok)
}
if b, ok := beat("")["freeagent_ok"]; ok {
t.Errorf("a beat with no DCN state probes no managed agent: %v", b)
}
}
// End to end with the real artifact: FREEAGENT_TEST_PYZ=/path/to/freeagent-x.pyz
func TestFaManagedEndToEnd(t *testing.T) {
pyzPath := os.Getenv("FREEAGENT_TEST_PYZ")
if pyzPath == "" {
t.Skip("set FREEAGENT_TEST_PYZ to the built freeagent .pyz")
}
if _, _, err := faPython(); err != nil {
t.Skip(err.Error())
}
withConfigDir(t)
pyz, err := os.ReadFile(pyzPath)
if err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(pyz)
srv := stubPublish(t, pyz, hex.EncodeToString(sum[:]))
old := faManifestURL
faManifestURL = srv.URL + "/bastion-agent/version.json"
t.Cleanup(func() { faManifestURL = old })
// a corpus and a signed grant with 'search'
corpus := t.TempDir()
os.WriteFile(filepath.Join(corpus, "note.txt"), []byte("routers and firewalls, mail m@example.com\n"), 0o600)
dcnDir := t.TempDir()
// As root the agent is DROPPED to the folder's owner (freeagent_drop.go):
// the corpus must belong to a real user, and the state base must be a
// traversable chain like /var/lib/privatae/freeagent is.
var owner *user.User
if processIsRoot() {
owner = faTestUser(t)
if owner == nil {
t.Skip("root, and no ordinary user account on this box to drop to")
}
uid, _ := strconv.Atoi(owner.Uid)
gid, _ := strconv.Atoi(owner.Gid)
makeTraversable(t, corpus)
for _, p := range []string{corpus, filepath.Join(corpus, "note.txt")} {
if err := os.Chown(p, uid, gid); err != nil {
t.Fatal(err)
}
}
base := t.TempDir()
makeTraversable(t, base)
oldBase := faStateBaseOverride
faStateBaseOverride = base
t.Cleanup(func() { faStateBaseOverride = oldBase })
}
pub, priv, _ := ed25519.GenerateKey(nil)
terms := map[string]any{"kind": "dcn_store_grant", "class": "machine", "server": "box",
"root": corpus, "mask": []string{"read", "search", "index"}, "excludes": []string{".git"},
"sweep": "manual", "expires": time.Now().Add(time.Hour).UTC().Format(time.RFC3339)}
rec := signedRec(t, priv, terms)
f := &dcnGrantFile{VerifyPubkey: base64.StdEncoding.EncodeToString(pub),
Stores: map[string]dcnGrantRec{"1": rec}}
if err := dcnSaveFile(dcnDir, f); err != nil {
t.Fatal(err)
}
faReconcile(dcnDir) // what the daemon does at start
t.Cleanup(func() { faStop(dcnDir, 1) })
stateDir, err := faStoreDir(dcnDir, 1)
if err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(120 * time.Second)
var c freeagentConfig
for time.Now().Before(deadline) {
faMgr.Lock()
inst := faMgr.inst[faKey{dcnDir, 1}]
state, detail := "", ""
if inst != nil {
state, detail = inst.State, inst.Detail
}
faMgr.Unlock()
if state == "failed" || state == "missing-python" {
t.Fatalf("managed install %s: %s", state, detail)
}
if cfg, ok := faManagedConfig(dcnDir, 1); ok {
c = cfg
break
}
time.Sleep(500 * time.Millisecond)
}
if c.Addr == "" {
st, _ := os.ReadFile(filepath.Join(stateDir, "serve.log"))
t.Fatalf("serve never wrote its port; serve.log:\n%s", st)
}
// health over mTLS with the box's identity
var body []byte
var status int
for time.Now().Before(deadline) {
status, body, err = freeagentCall(c, http.MethodGet, "/health", nil)
if err == nil && status == 200 {
break
}
time.Sleep(500 * time.Millisecond)
}
if err != nil || status != 200 || !strings.Contains(string(body), `"serving": true`) {
t.Fatalf("health: %d %v %s", status, err, body)
}
// the sweep ran: the catalog answers, sanitized
for time.Now().Before(deadline) {
status, body, err = freeagentCall(c, http.MethodGet, "/catalog", nil)
if err == nil && status == 200 && strings.Contains(string(body), "note.txt") {
break
}
time.Sleep(time.Second)
}
if status != 200 || !strings.Contains(string(body), "[PII_EMAIL_1]") || strings.Contains(string(body), "m@example.com") {
sl, _ := os.ReadFile(filepath.Join(stateDir, "sweep.log"))
t.Fatalf("catalog: %d %s\nsweep.log:\n%s", status, body, sl)
}
// files-and-folders: the agent lives in <root>/.agent, its output too, and
// the walk never indexed it; secrets stayed in the bastion's state dir
agentDir := filepath.Join(corpus, ".agent")
for _, f := range []string{"manifest.yaml", "catalog.jsonl", "walk.jsonl"} {
if _, err := os.Stat(filepath.Join(agentDir, f)); err != nil {
t.Fatalf("%s must live in <root>/.agent: %v", f, err)
}
}
if _, err := os.Stat(filepath.Join(agentDir, "sanitize", "vault.db")); err == nil {
t.Fatal("the vault must NOT be inside the folder")
}
if _, err := os.Stat(filepath.Join(stateDir, "vault.db")); err != nil {
t.Fatal("the vault must be in the bastion's state dir")
}
if _, err := os.Stat(filepath.Join(agentDir, "host.key")); err == nil {
t.Fatal("the identity key must NOT be inside the folder")
}
wm, _ := os.ReadFile(filepath.Join(agentDir, "walk.jsonl"))
if strings.Contains(string(wm), ".agent") {
t.Fatal("the walk must not index .agent")
}
gi, _ := os.ReadFile(filepath.Join(agentDir, ".gitignore"))
if !strings.Contains(string(gi), "!manifest.yaml") {
t.Fatal(".agent/.gitignore must keep the stores out of a member's repo")
}
if _, err := os.Stat(filepath.Join(stateDir, "out")); err == nil {
t.Fatal("pre-0.13.3 residue dir/out must be removed")
}
// the reply maps like any local agent reply
m, _, ok := freeagentMap("search", status, body, err)
if !ok || m["state"] != "ok" {
t.Fatal("mapping")
}
// the drop held: every child this daemon runs is the folder's owner, and
// the state it was handed belongs to that owner
if owner != nil {
faOwnChildren.Lock()
pids := []int{}
for pid := range faOwnChildren.pid {
pids = append(pids, pid)
}
faOwnChildren.Unlock()
if len(pids) == 0 {
t.Fatal("serve must be running as a tracked child")
}
for _, pid := range pids {
st, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pid), "status"))
if err != nil {
continue // exited between the snapshot and the read
}
for _, line := range strings.Split(string(st), "\n") {
if strings.HasPrefix(line, "Uid:") && !strings.Contains(line, "\t"+owner.Uid+"\t") {
t.Fatalf("child %d is not running as %s: %s", pid, owner.Username, line)
}
}
}
for _, p := range []string{stateDir, filepath.Join(stateDir, "host.key"), filepath.Join(stateDir, "vault.db"),
filepath.Join(agentDir, "manifest.yaml"), filepath.Join(agentDir, "catalog.jsonl")} {
if u, _, err := faPathOwner(p); err != nil || strconv.Itoa(int(u)) != owner.Uid {
t.Fatalf("%s must belong to %s (uid %d, %v)", p, owner.Username, u, err)
}
}
}
// the signature names a cert the child can read but not replace: on a
// root install that is the root-owned public copy
if owner != nil {
mf, _ := os.ReadFile(filepath.Join(agentDir, "manifest.yaml"))
_, _, pubDir, _ := faStoreDirs(dcnDir, 1)
want := "cert: " + filepath.Join(pubDir, "host.crt") + "\n"
if !strings.Contains(string(mf), want) {
t.Fatalf("the signature must name the public cert, want %q in:\n%s", want, mf)
}
if u, _, _ := faPathOwner(filepath.Join(pubDir, "host.crt")); u != 0 {
t.Fatal("the public cert must stay root-owned")
}
}
// heartbeat sees it
adv, allOK := faManagedProbe(dcnDir)
if !adv || !allOK {
t.Fatalf("managed probe: %v %v", adv, allOK)
}
st := faManagedStatus(dcnDir)["1"].(map[string]any)
if st["state"] != "running" || st["serving"] != true || st["version"] != "9.9.9" {
t.Fatalf("status: %v", st)
}
// a `manual` sweep runs ONCE — with a 1s backoff,
// a supervised sweep would have several runs on record by now
oldBackoff := faRestartBackoff
faRestartBackoff = time.Second
t.Cleanup(func() { faRestartBackoff = oldBackoff })
time.Sleep(3 * time.Second)
runs, _ := os.ReadFile(filepath.Join(corpus, ".agent", "run.jsonl"))
if n := strings.Count(string(runs), "\n"); n != 1 {
t.Fatalf("manual sweep must run exactly once, run.jsonl has %d lines", n)
}
// a re-approved grant with narrower terms restarts
// the instance under the new manifest (new excludes, new serve port)
before, _ := faSnapshot(dcnDir, 1)
terms2 := map[string]any{}
for k, v := range terms {
terms2[k] = v
}
terms2["excludes"] = []string{".git", ".ssh", "*.key"}
rec2 := signedRec(t, priv, terms2)
f.Stores["1"] = rec2
if err := dcnSaveFile(dcnDir, f); err != nil {
t.Fatal(err)
}
faReconcile(dcnDir)
deadline = time.Now().Add(60 * time.Second)
var after faInstance
for time.Now().Before(deadline) {
after, _ = faSnapshot(dcnDir, 1)
if after.State == "running" && after.Text != before.Text {
if c2, ok := faManagedConfig(dcnDir, 1); ok && c2.Addr != c.Addr {
break
}
}
time.Sleep(300 * time.Millisecond)
}
if after.Text == before.Text || !strings.Contains(after.Text, `"*.key"`) {
t.Fatalf("instance must restart under the re-approved terms; text unchanged: %q", after.Detail)
}
man, _ := os.ReadFile(after.Manifest)
if !strings.Contains(string(man), `".ssh"`) || !strings.Contains(string(man), "signature:") {
t.Fatal("re-rendered manifest must carry the new excludes and be re-signed")
}
// unchanged terms on the next reconcile: no restart (same port)
c3, _ := faManagedConfig(dcnDir, 1)
faReconcile(dcnDir)
time.Sleep(time.Second)
c4, _ := faManagedConfig(dcnDir, 1)
if c3.Addr != c4.Addr {
t.Fatal("identical terms must not restart the instance")
}
// A fresh identity with a surviving manifest (new layout, wiped base, a
// re-pair): the manifest must be re-signed under the new cert, or the
// gate refuses it and serve answers 503 forever.
faStop(dcnDir, 1)
_, privDir, _, _ := faStoreDirs(dcnDir, 1)
if err := os.RemoveAll(filepath.Join(privDir, "host.key")); err != nil {
t.Fatal(err)
}
_ = os.Remove(filepath.Join(privDir, "host.crt"))
before3 := after
faReconcile(dcnDir)
deadline = time.Now().Add(60 * time.Second)
var c9 freeagentConfig
for time.Now().Before(deadline) {
if cfg, ok := faManagedConfig(dcnDir, 1); ok && cfg.Addr != "" {
if st, _ := faSnapshot(dcnDir, 1); st.State == "running" && st.Cert != "" {
if _, body, err := freeagentCall(cfg, http.MethodGet, "/health", nil); err == nil && strings.Contains(string(body), `"serving": true`) {
c9 = cfg
break
}
}
}
if st, _ := faSnapshot(dcnDir, 1); st.State == "failed" {
t.Fatalf("re-identity: %s", st.Detail)
}
time.Sleep(500 * time.Millisecond)
}
if c9.Addr == "" {
sl, _ := os.ReadFile(filepath.Join(stateDir, "serve.log"))
t.Fatalf("after a fresh identity the manifest must be re-signed and served (was %q); serve.log:\n%s", before3.State, sl)
}
}
func TestFaChildEnvIsAnAllowlist(t *testing.T) {
t.Setenv("PRIVATAE_SECRET_LIKE_THING", "nope")
t.Setenv("PATH", "/usr/bin")
env := strings.Join(faChildEnv(&faInstance{Dir: t.TempDir(), Cert: "c", Key: "k"}), "\n")
if strings.Contains(env, "PRIVATAE_SECRET_LIKE_THING") {
t.Fatal("daemon environment must not leak to the child")
}
for _, want := range []string{"PATH=/usr/bin", "FREEAGENT_CA_ROOT=c", "FREEAGENT_TLS_KEY=k",
"EMBED_URL=http://127.0.0.1:11434", "FREEAGENT_VAULT_DIR="} {
if !strings.Contains(env, want) {
t.Fatalf("child env missing %s", want)
}
}
}
// The legacy name is retired on this side (0.42.0): one manifest address, one
// artifact name. Older bastions still read the legacy ones, which is the
// publish script's concern, not this binary's.
// recordingTransport answers every request 404 and records its URL, so a test
// can see every address a fetch tried, including ones it was never pointed at.
type recordingTransport struct{ urls []string }
func (r *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
r.urls = append(r.urls, req.URL.String())
return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil
}
func TestFaFetchManifestTriesOnlyTheNewAddress(t *testing.T) {
if faManifestURL != "https://privatae.ai/bastion-agent/version.json" {
t.Fatalf("manifest address = %q", faManifestURL)
}
// The production address, with nothing leaving the machine: every request
// is answered here. A failed fetch must not go on to any other address.
rec := &recordingTransport{}
oldTransport := http.DefaultTransport
http.DefaultTransport = rec
t.Cleanup(func() { http.DefaultTransport = oldTransport })
if _, _, _, err := faFetchManifest(); err == nil {
t.Fatal("a 404 must fail the fetch")
}
if len(rec.urls) != 1 || rec.urls[0] != faManifestURL {
t.Fatalf("requested %v; want only %s", rec.urls, faManifestURL)
}
}
func TestFaFetchManifestErrors(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/bad/version.json", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"version":"1.0.0","files":["not","a","map"]}`))
})
srv := httptest.NewServer(signedStub(releaseKindAgent, mux))
t.Cleanup(srv.Close)
old := faManifestURL
t.Cleanup(func() { faManifestURL = old })
// Signed and versioned, but not a manifest this reads: refused, not read
// as an empty one.
faManifestURL = srv.URL + "/bad/version.json"
if _, _, _, err := faFetchManifest(); err == nil || !strings.Contains(err.Error(), "malformed") {
t.Fatalf("a malformed manifest must be refused, got %v", err)
}
// Not served: the error names the address that failed.
faManifestURL = srv.URL + "/nope/version.json"
if _, _, _, err := faFetchManifest(); err == nil || !strings.Contains(err.Error(), "/nope/") {
t.Fatalf("want the failing address in the error, got %v", err)
}
}
func TestFaEnsurePyzReadsOnlyTheNewArtifactName(t *testing.T) {
body := []byte("#!/usr/bin/env python3\nfake")
sum := sha256.Sum256(body)
good := hex.EncodeToString(sum[:])
old := faManifestURL
t.Cleanup(func() { faManifestURL = old })
// A DISTINCT version per case, deliberately. faEnsurePyz returns the cached
// copy early when the installed version matches the manifest's, so reusing
// one version would let a later case pass without reaching the name lookup.
serve := func(files map[string]string, version string) (*httptest.Server, chan string) {
mux := http.NewServeMux()
var srv *httptest.Server
served := make(chan string, 4)
mux.HandleFunc("/m.json", func(w http.ResponseWriter, r *http.Request) {
m := map[string]any{}
for name, path := range files {
m[name] = map[string]any{"url": srv.URL + path, "sha256": good}
}
json.NewEncoder(w).Encode(map[string]any{"version": version, "files": m})
})
for _, path := range files {
path := path
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { served <- path; w.Write(body) })
}
srv = httptest.NewServer(signedStub(releaseKindAgent, mux))
t.Cleanup(srv.Close)
return srv, served
}
withConfigDir(t)
srv, _ := serve(map[string]string{"bastion-agent-1.1.1.pyz": "/new"}, "1.1.1")
faManifestURL = srv.URL + "/m.json"
if _, v, err := faEnsurePyz(); err != nil || v != "1.1.1" {
t.Fatalf("the artifact name must install: %v %q", err, v)
}
// Both names, as the publish script writes while older bastions need the
// legacy one. The URL proves which entry was downloaded.
withConfigDir(t)
srv, served := serve(map[string]string{"bastion-agent-4.4.4.pyz": "/new", "freeagent-4.4.4.pyz": "/legacy"}, "4.4.4")
faManifestURL = srv.URL + "/m.json"
if _, v, err := faEnsurePyz(); err != nil || v != "4.4.4" {
t.Fatalf("both names present must install: %v %q", err, v)
}
select {
case which := <-served:
if which != "/new" {
t.Fatalf("downloaded %s; only the new name is read", which)
}
default:
t.Fatal("nothing was downloaded")
}
// Only the legacy name: refused, and the error names what was sought.
withConfigDir(t)
srv, served = serve(map[string]string{"freeagent-2.2.2.pyz": "/legacy"}, "2.2.2")
faManifestURL = srv.URL + "/m.json"
_, _, err := faEnsurePyz()
if err == nil || !strings.Contains(err.Error(), "bastion-agent-2.2.2.pyz") {
t.Fatalf("a manifest with only the legacy name must be refused, got %v", err)
}
select {
case which := <-served:
t.Fatalf("downloaded %s from a manifest it should have refused", which)
default:
}
}