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")
}