internal/server/api/trustedcas_test.go
Ref: Size: 5.1 KiB History
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/a73x/eitri/internal/server/api/types"
"github.com/a73x/eitri/internal/server/sshca"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// createVMFor creates a VM on hostID as the holder of pat and returns its id.
func createVMFor(t *testing.T, ts *httptest.Server, pat, hostID string) string {
t.Helper()
resp := do(t, "POST", ts.URL+"/api/v1/vms", pat, map[string]any{"host_id": hostID})
require.Equal(t, http.StatusCreated, resp.StatusCode)
var out struct {
ID string `json:"id"`
}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&out))
return out.ID
}
// getVM reads one VM back off the list endpoint as the holder of pat — the
// same route the console renders every VM from, so what this sees is what the
// page sees.
func getVM(t *testing.T, ts *httptest.Server, pat, id string) types.VM {
t.Helper()
resp := do(t, "GET", ts.URL+"/api/v1/vms", pat, nil)
require.Equal(t, http.StatusOK, resp.StatusCode)
var vms []types.VM
require.NoError(t, json.NewDecoder(resp.Body).Decode(&vms))
for _, vm := range vms {
if vm.ID == id {
return vm
}
}
t.Fatalf("vm %s absent from the list", id)
return types.VM{}
}
// uploadCA registers a freshly generated user CA under label and returns its
// canonical authorized_keys line.
func uploadCA(t *testing.T, ts *httptest.Server, pat, label string) string {
t.Helper()
_, signer, err := sshca.GenerateHostKey()
require.NoError(t, err)
line := sshca.AuthorizedKeyLine(signer.PublicKey())
resp := do(t, "POST", ts.URL+"/api/v1/user-cas", pat,
map[string]any{"public_key": line, "label": label})
require.Equal(t, http.StatusCreated, resp.StatusCode)
return line
}
// TestCreatedVMNamesTheCAsItTrusts pins the fact the VM page exists to show:
// the set is on the object, by the label the tenant gave it, and it is the set
// that was registered when the VM was made.
func TestCreatedVMNamesTheCAsItTrusts(t *testing.T) {
ts, st, _, _, _ := newServer(t)
pat, hostID := caLessTenant(t, ts, st, "trusts")
uploadCA(t, ts, pat, "laptop")
vm := getVM(t, ts, pat, createVMFor(t, ts, pat, hostID))
require.NotNil(t, vm.TrustedCAs, "a VM created now records what it trusts")
require.Len(t, *vm.TrustedCAs, 1)
assert.Equal(t, "laptop", (*vm.TrustedCAs)[0].Label)
assert.NotEmpty(t, (*vm.TrustedCAs)[0].Fingerprint, "the fingerprint is what an operator matches by eye")
}
// TestALaterCADoesNotJoinAnExistingVMsTrust is the create-time freeze as the
// API tells it, and the same property the snapshot test pins one layer down.
// If this ever fails, the 409 that refuses a CA-less tenant is telling users
// something untrue about their own fleet.
func TestALaterCADoesNotJoinAnExistingVMsTrust(t *testing.T) {
ts, st, _, _, _ := newServer(t)
pat, hostID := caLessTenant(t, ts, st, "frozen")
uploadCA(t, ts, pat, "laptop")
id := createVMFor(t, ts, pat, hostID)
uploadCA(t, ts, pat, "ci") // registered AFTER the VM exists
vm := getVM(t, ts, pat, id)
require.NotNil(t, vm.TrustedCAs)
require.Len(t, *vm.TrustedCAs, 1, "the guest's trust was fixed when it was created")
assert.Equal(t, "laptop", (*vm.TrustedCAs)[0].Label)
// And the fuller set does reach the next VM, so the freeze is per-VM rather
// than the tenant's set having simply stopped growing.
later := getVM(t, ts, pat, createVMFor(t, ts, pat, hostID))
require.NotNil(t, later.TrustedCAs)
assert.Len(t, *later.TrustedCAs, 2)
}
// TestTrustedCAsCarryNoKeyMaterial pins that the wire shows the two facts a
// reader needs and not the CA lines themselves. They are public keys, so this
// is not a secrecy boundary — it is the object answering the question it was
// asked instead of shipping the agent's copy to every console.
func TestTrustedCAsCarryNoKeyMaterial(t *testing.T) {
ts, st, _, _, _ := newServer(t)
pat, hostID := caLessTenant(t, ts, st, "nomaterial")
line := uploadCA(t, ts, pat, "laptop")
createVMFor(t, ts, pat, hostID)
resp := do(t, "GET", ts.URL+"/api/v1/vms", pat, nil)
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.NotContains(t, bodyText(t, resp), line,
"the VM object names its CAs; it does not carry their key lines")
}
// TestTrustedCAsDoNotCrossTenants pins the isolation. The set rides a
// tenant-scoped object, so the leak this guards against would be a server-side
// mix-up rather than a missing authz check — one tenant's CA labels and
// fingerprints appearing on another tenant's guest.
func TestTrustedCAsDoNotCrossTenants(t *testing.T) {
ts, st, _, _, _ := newServer(t)
patA, hostA := caLessTenant(t, ts, st, "tenant-a")
patB, hostB := caLessTenant(t, ts, st, "tenant-b")
uploadCA(t, ts, patA, "a-laptop")
uploadCA(t, ts, patB, "b-laptop")
vmA := getVM(t, ts, patA, createVMFor(t, ts, patA, hostA))
vmB := getVM(t, ts, patB, createVMFor(t, ts, patB, hostB))
require.NotNil(t, vmA.TrustedCAs)
require.NotNil(t, vmB.TrustedCAs)
require.Len(t, *vmA.TrustedCAs, 1)
require.Len(t, *vmB.TrustedCAs, 1)
assert.Equal(t, "a-laptop", (*vmA.TrustedCAs)[0].Label)
assert.Equal(t, "b-laptop", (*vmB.TrustedCAs)[0].Label)
assert.NotEqual(t, (*vmA.TrustedCAs)[0].Fingerprint, (*vmB.TrustedCAs)[0].Fingerprint)
}