Ctrl K
ComunidadGuías…

failopen_container_access_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"context"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/pingular/pingularity/internal/config"
	"github.com/pingular/pingularity/internal/settings"
	"github.com/pingular/pingularity/internal/store"
)

// These tests pin the fail-CLOSED contract for container access. The daemon
// used to persist access_local_only=false for a container whose store merely
// LOOKED old (established, no birth marker, no stored access key). The birth
// marker landed AFTER the fail-closed default did, so a container first
// installed by a build from that window was born PRIVATE and still carries no
// marker, leaving it byte-identical on disk to a genuine 0.61-or-earlier
// install. Every gate passed, and the upgrade silently put an unauthenticated
// dashboard on the LAN (default listen is every interface, auth usually off).
//
// Nothing on disk separates the two populations, so both now fail closed: the
// daemon WARNS and leaves access local-only. Recovery is explicit and
// authoritative at every boot - PINGULARITY_ACCESS=network via reconcileAccess.

// warnCall is one captured WARN from warnAmbiguousContainerAccess.
type warnCall struct {
	msg  string
	args []any
}

// bootAccessDecision replays main's boot-time access sequence in the same order
// run() does: the ambiguity warning (which must never write) followed by
// reconcileAccess (the explicit-input recovery path, which may). It returns
// every warning raised, so a test can assert both the state the boot ends in
// and what the operator was told.
func bootAccessDecision(t *testing.T, cfg config.Config, st *store.Store, set *settings.Controller, inContainer bool) []warnCall {
	t.Helper()
	ctx := context.Background()
	var warns []warnCall
	if err := warnAmbiguousContainerAccess(ctx, cfg, st, set, inContainer, func(msg string, args ...any) {
		warns = append(warns, warnCall{msg: msg, args: args})
	}); err != nil {
		t.Fatalf("warnAmbiguousContainerAccess: %v", err)
	}
	if _, err := reconcileAccess(ctx, cfg, set); err != nil {
		t.Fatalf("reconcileAccess: %v", err)
	}
	return warns
}

// settingsSnapshot is every persisted settings key/value, for proving a boot
// wrote NOTHING - the access key included, but not only it.
func settingsSnapshot(t *testing.T, st *store.Store) map[string]string {
	t.Helper()
	all, err := st.AllSettings(context.Background())
	if err != nil {
		t.Fatalf("AllSettings: %v", err)
	}
	return all
}

// storedAccessKey reports whether access_local_only is PRESENT in the store -
// key presence, the signal the daemon gates on, distinct from the value the
// controller's overlay answers when nothing is stored.
func storedAccessKey(t *testing.T, st *store.Store) bool {
	t.Helper()
	_, ok := settingsSnapshot(t, st)["access_local_only"]
	return ok
}

// testDefaults are the daemon's fresh-install seeds in miniature:
// AccessLocalOnly true (fail closed) seeded as a DEFAULT, never stored -
// exactly what every boot looks like before anyone chooses.
func testDefaults() settings.Values { return testDefaultsFor(config.Config{}) }

// testDefaultsFor models how main SEEDS the controller: `def :=
// defaultSettings(p.cfg)`, so the access scope in the DEFAULTS comes from the
// same -access/PINGULARITY_ACCESS the boot is running with. A fixture that
// hardcodes local-only while passing network to reconcileAccess manufactures a
// disagreement production never has, and would "prove" a persistence that does
// not happen. The mirror is checked against defaultSettings itself below, so it
// cannot drift.
func testDefaultsFor(cfg config.Config) settings.Values {
	return settings.Values{
		Latency: 5 * time.Second, Speed: time.Hour, Timeout: 2 * time.Second,
		DownAfter: 3, UpAfter: 2, AccessLocalOnly: defaultSettings(cfg).AccessLocalOnly, Monitoring: true,
	}
}

func openStore(t *testing.T) *store.Store {
	t.Helper()
	st, err := store.Open(":memory:")
	if err != nil {
		t.Fatalf("open store: %v", err)
	}
	t.Cleanup(func() { st.Close() })
	return st
}

// newPreMarkerContainerStore builds the ambiguous shape: operator configuration
// and measurement history (so the store reads as ESTABLISHED), NO birth marker,
// and NO stored access key. Every population in the header lands here - a
// 0.61-or-earlier upgrade, a build that predates the marker, a lost birth
// stamp. Ordering is load-bearing: the store is established BEFORE
// settings.New runs, so New refuses to stamp the marker (ensureBornMarker only
// stamps a brand-new store), reproducing the on-disk shape rather than asserting it.
func newPreMarkerContainerStore(t *testing.T) (*store.Store, *settings.Controller) {
	t.Helper()
	return newAmbiguousContainerStore(t, config.Config{})
}

// newAmbiguousContainerStore is the same shape seeded as main would seed it for
// the boot under test: cfg decides the controller's default access scope, so a
// boot carrying PINGULARITY_ACCESS=network starts with the value already in
// effect - which is exactly why the explicit recovery writes nothing.
func newAmbiguousContainerStore(t *testing.T, cfg config.Config) (*store.Store, *settings.Controller) {
	t.Helper()
	st := openStore(t)
	return st, establishedContainerController(t, st, cfg)
}

// establishedContainerController gives st the ordinary life that makes a store
// read ESTABLISHED - operator configuration and measurement history - and then
// builds the controller for the boot under test. It is the ONLY way the
// container-access tests build one, deliberately: the defaults come from
// testDefaultsFor(cfg), mirroring main's `def := defaultSettings(p.cfg)`, and
// the mirror is checked here rather than trusted. A fixture that hardcoded
// local-only defaults while passing an explicit network scope to reconcileAccess
// would manufacture a disagreement production never has, and would "prove" a
// persistence that does not happen - so that shape is not reachable from here.
//
// Ordering is load-bearing: the store is established BEFORE settings.New runs,
// so New refuses to stamp the marker and the on-disk shape is reproduced rather
// than asserted.
func establishedContainerController(t *testing.T, st *store.Store, cfg config.Config) *settings.Controller {
	t.Helper()
	ctx := context.Background()
	if err := st.SetSetting(ctx, "monitoring", "true"); err != nil {
		t.Fatalf("seed operator configuration: %v", err)
	}
	if err := st.InsertSamples(ctx, []store.Sample{
		{TS: time.Now().Add(-12 * time.Hour), Target: "cloudflare", Family: "ipv4", LatencyMS: 11, Success: true},
	}); err != nil {
		t.Fatalf("seed history: %v", err)
	}
	set, err := settings.New(ctx, st, testDefaultsFor(cfg), settings.WithBornVersion("0.62.0-test"))
	if err != nil {
		t.Fatalf("new settings: %v", err)
	}
	// Fixture preconditions: this is the ambiguous shape, or the tests below
	// prove nothing.
	all := settingsSnapshot(t, st)
	if _, born := all[settings.KeyInstallBornVersion]; born {
		t.Fatal("fixture stamped a birth marker; a pre-marker store has none")
	}
	if _, stored := all["access_local_only"]; stored {
		t.Fatal("fixture stored an access choice; a pre-marker store has none")
	}
	if want := defaultSettings(cfg).AccessLocalOnly; set.AccessLocalOnly() != want {
		t.Fatalf("fixture seeded AccessLocalOnly=%v, but main would seed %v for this config", set.AccessLocalOnly(), want)
	}
	return set
}

// THE REPRO. A container whose store carries history but no birth marker and no
// stored access choice must end the boot still local-only, with NOTHING
// written. Against the removed grandfather this store satisfied every gate - in
// a container, established, no marker, no stored key, no explicit flag - so it
// persisted access_local_only=false and published the dashboard to the LAN.
//
// ONE test covers every install that reaches this shape, deliberately: a
// genuine 0.61-or-earlier upgrade, an install from a build that predates the
// marker, and one whose birth stamp failed to write. They are the same bytes
// on disk, which is the entire finding - no rule can open one without opening
// the others, so the only safe answer is to open none of them. (A separate root
// test covers the lost-stamp fixture end to end; a per-population copy here
// would assert the same state twice and imply a distinction that does not
// exist.)
func TestAmbiguousContainerUpgradeFailsClosedAndWritesNothing(t *testing.T) {
	ctx := context.Background()
	st, set := newPreMarkerContainerStore(t)
	before := settingsSnapshot(t, st)

	warns := bootAccessDecision(t, config.Config{}, st, set, true) // no -access/PINGULARITY_ACCESS

	if !set.AccessLocalOnly() {
		t.Fatal("a pre-marker container was opened to the network on upgrade; ambiguous provenance must fail CLOSED")
	}
	if storedAccessKey(t, st) {
		t.Fatal("an access choice was persisted from an inference about the store's age; nothing may write access but explicit operator input")
	}
	if after := settingsSnapshot(t, st); !reflect.DeepEqual(before, after) {
		t.Fatalf("the boot wrote to settings: before=%v after=%v (the ambiguity path must write nothing at all)", before, after)
	}
	// Not just held in memory: the next boot must read the same fail-closed state.
	if err := set.Reload(ctx); err != nil {
		t.Fatalf("reload: %v", err)
	}
	if !set.AccessLocalOnly() {
		t.Fatal("access did not survive a reload as local-only")
	}
	if len(warns) != 1 {
		t.Fatalf("got %d warnings, want exactly 1 - the operator must be told why their published port 403s", len(warns))
	}
}

// THE RECOVERY. The same ambiguous container, restarted with an explicit
// PINGULARITY_ACCESS=network, is opened - by reconcileAccess, on operator
// input, and persisted. This is what makes failing closed affordable: an
// upgraded container that really was reachable gets back with one env var and
// no shell in the image.
func TestExplicitAccessStillDecidesTheAmbiguousContainer(t *testing.T) {
	ctx := context.Background()

	// Seeded the way main seeds it, the env var is already the effective value,
	// so reconcileAccess finds nothing to change and writes NOTHING. The port
	// opens for this boot and every later one that carries the variable - but
	// the choice is not in the database, which is why the docs tell operators to
	// keep the variable (or make it stick from the Access tab) rather than
	// assuming one run was enough. Asserting a write here would have pinned a
	// persistence production does not do.
	t.Run("explicit network opens the boot without writing a stored choice", func(t *testing.T) {
		cfg := config.Config{Access: "network", AccessExplicit: true}
		st, set := newAmbiguousContainerStore(t, cfg)

		warns := bootAccessDecision(t, cfg, st, set, true)

		if set.AccessLocalOnly() {
			t.Fatal("explicit PINGULARITY_ACCESS=network did not open access; that is the documented way out of the lockout")
		}
		if storedAccessKey(t, st) {
			t.Fatal("the boot wrote an access row: with the scope already seeded from the same config there is nothing to reconcile, and claiming otherwise is what made the old test model a state production never reaches")
		}
		if len(warns) != 0 {
			t.Fatalf("got %d warnings, want 0: access is settled, so there is no ambiguity to report", len(warns))
		}
	})

	// The write DOES happen where there is a real disagreement to settle: a
	// stored local-only that the operator overrides with an explicit network
	// scope. That is reconcileAccess earning its keep, and it must persist.
	t.Run("explicit network overrides a disagreeing stored choice, and persists", func(t *testing.T) {
		cfg := config.Config{Access: "network", AccessExplicit: true}
		st, set := newAmbiguousContainerStore(t, cfg)
		if err := st.SetSetting(ctx, "access_local_only", "true"); err != nil {
			t.Fatalf("seed a stored local-only choice: %v", err)
		}
		if err := set.Reload(ctx); err != nil {
			t.Fatalf("reload: %v", err)
		}
		if !set.AccessLocalOnly() {
			t.Fatal("precondition: the stored local-only choice must be in effect before the override")
		}

		bootAccessDecision(t, cfg, st, set, true)

		if set.AccessLocalOnly() {
			t.Fatal("the explicit network scope did not override the stored local-only choice")
		}
		if err := set.Reload(ctx); err != nil {
			t.Fatalf("reload: %v", err)
		}
		if set.AccessLocalOnly() {
			t.Fatal("the override did not survive a reload; it was never written")
		}
	})

	t.Run("explicit local keeps it private, silently", func(t *testing.T) {
		st, set := newPreMarkerContainerStore(t)
		cfg := config.Config{Access: "local", AccessExplicit: true}

		warns := bootAccessDecision(t, cfg, st, set, true)

		if !set.AccessLocalOnly() {
			t.Fatal("explicit -access local must keep the install private")
		}
		if len(warns) != 0 {
			t.Fatalf("got %d warnings, want 0 when the operator stated the access mode", len(warns))
		}
	})
}

// A container born under a build that STAMPS the birth marker carries one, and
// every build that stamps it already defaulted closed - so its missing access
// key means "never chose", not "used to be reachable". It boots untouched,
// private, and unlectured - even after consenting without an access choice
// (Quick Setup dismissal) and accruing history, which is what makes it look
// established from its second boot on.
func TestMarkedContainerStaysPrivateAndUnwarned(t *testing.T) {
	ctx := context.Background()
	st := openStore(t)

	// Boot one: brand-new store, initialized fail-closed and marked.
	set1, err := settings.New(ctx, st, testDefaults(), settings.WithBornVersion("0.62.0-test"))
	if err != nil {
		t.Fatalf("new settings (boot one): %v", err)
	}
	if _, born := settingsSnapshot(t, st)[settings.KeyInstallBornVersion]; !born {
		t.Fatal("a brand-new store must carry the birth marker; the provenance gate depends on it")
	}
	if err := set1.EnsureQuickSetupOffer(ctx, time.Now().Unix()); err != nil {
		t.Fatalf("EnsureQuickSetupOffer: %v", err)
	}
	// Dismissed Quick Setup: answered, but no access choice stored.
	if err := set1.SetQuickSetupDone(ctx, true); err != nil {
		t.Fatalf("SetQuickSetupDone: %v", err)
	}
	if err := st.InsertSamples(ctx, []store.Sample{
		{TS: time.Now(), Target: "cloudflare", Family: "ipv4", LatencyMS: 12, Success: true},
	}); err != nil {
		t.Fatalf("InsertSamples: %v", err)
	}

	// Boot two: same store, same defaults, no explicit -access.
	set2, err := settings.New(ctx, st, testDefaults(), settings.WithBornVersion("0.62.0-test"))
	if err != nil {
		t.Fatalf("new settings (boot two): %v", err)
	}
	before := settingsSnapshot(t, st)

	warns := bootAccessDecision(t, config.Config{}, st, set2, true)

	if !set2.AccessLocalOnly() {
		t.Fatal("a container born with a birth marker lost its fail-closed access")
	}
	if storedAccessKey(t, st) {
		t.Fatal("an access key was stored for an install whose operator never chose")
	}
	if after := settingsSnapshot(t, st); !reflect.DeepEqual(before, after) {
		t.Fatalf("the boot wrote to settings: before=%v after=%v", before, after)
	}
	if len(warns) != 0 {
		t.Fatalf("got %d warnings, want 0: a marked store's provenance is known, so there is nothing ambiguous to warn about", len(warns))
	}
}

// The firing matrix: exactly which shapes get the WARN, and its content. A
// warning that fires everywhere is noise operators learn to skip, and this one
// is the only notice an upgraded container gets before its port starts
// answering 403.
func TestWarnAmbiguousContainerAccessFires(t *testing.T) {
	ctx := context.Background()

	// ambiguous: established container store, no marker, no stored access key.
	ambiguous := func(t *testing.T) (*store.Store, *settings.Controller) { return newPreMarkerContainerStore(t) }
	// marked: born under a build that stamps the marker, established by later
	// history.
	marked := func(t *testing.T) (*store.Store, *settings.Controller) {
		t.Helper()
		st := openStore(t)
		set, err := settings.New(ctx, st, testDefaults(), settings.WithBornVersion("0.62.0-test"))
		if err != nil {
			t.Fatalf("new settings: %v", err)
		}
		if err := st.InsertSamples(ctx, []store.Sample{
			{TS: time.Now(), Target: "cloudflare", Family: "ipv4", LatencyMS: 8, Success: true},
		}); err != nil {
			t.Fatalf("InsertSamples: %v", err)
		}
		return st, set
	}
	// chosen: the ambiguous shape, except the operator already stored a choice.
	chosen := func(t *testing.T) (*store.Store, *settings.Controller) {
		t.Helper()
		st, set := newPreMarkerContainerStore(t)
		if err := set.SetAccessLocalOnly(ctx, true); err != nil {
			t.Fatalf("SetAccessLocalOnly: %v", err)
		}
		return st, set
	}
	// fresh: brand-new container, nothing at all in the store.
	fresh := func(t *testing.T) (*store.Store, *settings.Controller) {
		t.Helper()
		st := openStore(t)
		set, err := settings.New(ctx, st, testDefaults(), settings.WithBornVersion("0.62.0-test"))
		if err != nil {
			t.Fatalf("new settings: %v", err)
		}
		return st, set
	}

	cases := []struct {
		name      string
		fixture   func(*testing.T) (*store.Store, *settings.Controller)
		cfg       config.Config
		container bool
		want      bool
	}{
		{"ambiguous container: the one shape that gets it", ambiguous, config.Config{}, true, true},
		{"marker present: provenance known", marked, config.Config{}, true, false},
		{"access already chosen and stored", chosen, config.Config{}, true, false},
		{"explicit -access network settles it", ambiguous, config.Config{Access: "network", AccessExplicit: true}, true, false},
		{"explicit -access local settles it", ambiguous, config.Config{Access: "local", AccessExplicit: true}, true, false},
		{"native install: the filter always defaulted on", ambiguous, config.Config{}, false, false},
		{"fresh container: never was reachable", fresh, config.Config{}, true, false},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			st, set := c.fixture(t)
			var warns []warnCall
			if err := warnAmbiguousContainerAccess(ctx, c.cfg, st, set, c.container, func(msg string, args ...any) {
				warns = append(warns, warnCall{msg: msg, args: args})
			}); err != nil {
				t.Fatalf("warnAmbiguousContainerAccess: %v", err)
			}
			if got := len(warns) > 0; got != c.want {
				t.Fatalf("warned = %v, want %v", got, c.want)
			}
			if !c.want {
				return
			}
			if warns[0].msg != accessAmbiguousWarnMsg {
				t.Errorf("warning message = %q, want %q", warns[0].msg, accessAmbiguousWarnMsg)
			}
			// The line has to be actionable on its own: it must name the state
			// and every way out, because the log is all the operator has once
			// the dashboard 403s them.
			line := warns[0].msg
			for _, a := range warns[0].args {
				if s, ok := a.(string); ok {
					line += " " + s
				}
			}
			line = strings.ToLower(line)
			for _, want := range []string{"local-only", "-access network", "pingularity_access=network", "password", "access tab"} {
				if !strings.Contains(line, want) {
					t.Errorf("warning does not mention %q; operators recover from this line alone:\n%s", want, line)
				}
			}
		})
	}
}

// A store read decides nothing: an unreadable store returns the error (the
// caller logs it and re-judges next boot) and neither warns nor writes.
func TestWarnAmbiguousContainerAccessReadErrorDecidesNothing(t *testing.T) {
	ctx := context.Background()
	st, set := newPreMarkerContainerStore(t)
	st.Close() // the store goes away under the daemon

	var warns int
	err := warnAmbiguousContainerAccess(ctx, config.Config{}, st, set, true, func(string, ...any) { warns++ })
	if err == nil {
		t.Fatal("a failed store read must be reported, not swallowed")
	}
	if warns != 0 {
		t.Fatalf("warned %d times on a read error; nothing was established", warns)
	}
	if !set.AccessLocalOnly() {
		t.Fatal("access changed on a read error")
	}
}