a73x

internal/server/syncsvc/trustedcas_test.go

Ref:   Size: 6.1 KiB   History

package syncsvc

import (
	"testing"
	"time"

	"github.com/a73x/eitri/internal/server/hub"
	"github.com/a73x/eitri/internal/server/registry"
	"github.com/a73x/eitri/internal/server/store"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	caLaptop = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAALAPTOP"
	caCI     = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAACIAAAA"
)

// trustedVM places a VM on the fixture's host carrying the given frozen CA set.
// A nil set writes a row with no record, which is what every VM created before
// the column existed looks like.
func trustedVM(t *testing.T, f *fixture, id, name string, cas []store.TrustedCA) {
	t.Helper()
	require.NoError(t, f.st.CreateVM(store.VM{ID: id, HostID: f.host.ID, Name: name,
		ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5,
		PowerState: "running", TrustedCAs: cas}))
}

// snapCAs returns the CA lines the snapshot carries for vmID.
func snapCAs(t *testing.T, f *fixture, vmID string) []string {
	t.Helper()
	snap, err := f.svc.buildSnapshot(f.host.ID)
	require.NoError(t, err)
	for _, v := range snap.GetVms() {
		if v.GetVmId() == vmID {
			return v.GetSshUserCaAuthorizedKeys()
		}
	}
	t.Fatalf("vm %s absent from snapshot", vmID)
	return nil
}

// TestSnapshotServesTheFrozenSetNotTheLiveOne is the property the whole feature
// rests on, and the one the create-time refusal has always claimed: a CA
// registered after a VM exists does not reach that VM. It used to — the
// snapshot read the tenant's current set on every push, so a guest still
// waiting on an image download or a host certificate would bake whatever had
// arrived by then.
func TestSnapshotServesTheFrozenSetNotTheLiveOne(t *testing.T) {
	f := setup(t)
	trustedVM(t, f, "vm1", "web-1", []store.TrustedCA{{Label: "laptop", AuthorizedKey: caLaptop}})

	// The tenant registers a second CA AFTER the VM was created.
	require.NoError(t, f.st.AddTenantUserCA(testTenant, caCI, "tenant", "ci", "admin"))

	assert.Equal(t, []string{caLaptop}, snapCAs(t, f, "vm1"),
		"a CA registered after create must not reach a guest that already exists")
}

// TestSnapshotGivesALaterVMTheLaterSet is the other half: the freeze is per-VM,
// not fleet-wide, so a VM created after the upload trusts the fuller set. Both
// VMs sit on one host and one snapshot, which is where a shared cache would
// have leaked one VM's set into the other.
func TestSnapshotGivesALaterVMTheLaterSet(t *testing.T) {
	f := setup(t)
	trustedVM(t, f, "vm1", "web-1", []store.TrustedCA{{Label: "laptop", AuthorizedKey: caLaptop}})
	trustedVM(t, f, "vm2", "web-2", []store.TrustedCA{
		{Label: "laptop", AuthorizedKey: caLaptop},
		{Label: "ci", AuthorizedKey: caCI},
	})

	assert.Equal(t, []string{caLaptop}, snapCAs(t, f, "vm1"))
	assert.Equal(t, []string{caLaptop, caCI}, snapCAs(t, f, "vm2"))
}

// TestSnapshotFallsBackToTheLiveSetForAnUnrecordedVM pins the migration edge. A
// row written before the column has no set to serve, and serving an empty one
// would seed a guest that trusts no CA at all — unreachable for good. The
// fallback is the pre-freeze behaviour, kept for exactly the VM that was
// mid-create when the server rolled.
func TestSnapshotFallsBackToTheLiveSetForAnUnrecordedVM(t *testing.T) {
	f := setup(t)
	require.NoError(t, f.st.AddTenantUserCA(testTenant, caLaptop, "tenant", "laptop", "admin"))
	trustedVM(t, f, "vm1", "web-1", nil)

	assert.Equal(t, []string{caLaptop}, snapCAs(t, f, "vm1"))
}

// TestSnapshotDoesNotCrossTenantsOnTheFallback guards the one path that still
// reads a live set: it must read the VM's OWN tenant. A fleet-wide read here
// would hand one tenant's CA to another tenant's guest, which is the failure
// the create-side refusal was tested against for the same reason.
func TestSnapshotDoesNotCrossTenantsOnTheFallback(t *testing.T) {
	f := setup(t)
	other, err := f.st.CreateTenantForIdentity("https://test-issuer", "other-subject", "other@test.local")
	require.NoError(t, err)
	require.NoError(t, f.st.AddTenantUserCA(other.ID, caCI, "tenant", "someone-elses", "admin"))
	require.NoError(t, f.st.AddTenantUserCA(testTenant, caLaptop, "tenant", "laptop", "admin"))
	trustedVM(t, f, "vm1", "web-1", nil)

	// Exactly the host tenant's own CA: asserting the set rather than the
	// absence keeps this from passing if the fallback stopped serving anything.
	assert.Equal(t, []string{caLaptop}, snapCAs(t, f, "vm1"),
		"another tenant's CA must not be served to this tenant's guest")
}

// TestSnapshotServesTheSetFrozenAtUpgrade is the legacy VM's version of the
// same promise, end to end. A row written before the freeze existed has its set
// recorded by the store's backfill when the server upgrades; from then on the
// snapshot serves THAT set, and a CA the tenant registers afterwards does not
// reach the guest — where before the upgrade it would have, on every push, for
// the rest of that VM's life.
func TestSnapshotServesTheSetFrozenAtUpgrade(t *testing.T) {
	path := t.TempDir() + "/db"

	// The database the old server leaves: one VM, no recorded set, one CA.
	old, err := store.Open(path, "10.77.0.0/16")
	require.NoError(t, err)
	seedTestTenant(t, old)
	tok, _ := old.CreateEnrollmentToken(testTenant)
	host, err := old.RedeemEnrollmentToken(tok, store.EnrollFacts{Name: "h", OS: "linux",
		Arch: "amd64", Provisioner: "cloudhv", Remote: ""})
	require.NoError(t, err)
	require.NoError(t, old.AddTenantUserCA(testTenant, caLaptop, "tenant", "laptop", "admin"))
	require.NoError(t, old.CreateVM(store.VM{ID: "vm1", HostID: host.ID, Name: "web-1",
		ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
	require.NoError(t, old.Close())

	// The upgrade: Open freezes the set that row was being served.
	st, err := store.Open(path, "10.77.0.0/16")
	require.NoError(t, err)
	t.Cleanup(func() { st.Close() })
	svc := newWithWriteTimeout(st, registry.New(time.Now), hub.New(), []byte("s3cret"), 0, 0)
	f := &fixture{st: st, host: host, svc: svc}

	require.NoError(t, st.AddTenantUserCA(testTenant, caCI, "tenant", "ci", "admin"))
	assert.Equal(t, []string{caLaptop}, snapCAs(t, f, "vm1"),
		"a CA registered after the upgrade must not reach a guest the upgrade froze")
}