package host import ( "bytes" "context" "encoding/json" "errors" "net" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14FenceViewDeadline(t *testing.T) { t.Parallel() for _, lateSuccess := range []bool{false, true} { t.Run(map[bool]string{false: "timeout", true: "late success"}[lateSuccess], func(t *testing.T) { captured := hostguard.Captured{Incarnation: "inc", Stamp: 1} read := rootFenceViewClient(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { deadline, ok := ctx.Deadline() if !ok || time.Until(deadline) > time.Second { t.Fatal("fence view has no short deadline") } <-ctx.Done() if !lateSuccess { return hostbox.JournalResponse{}, ctx.Err() } return hostbox.JournalResponse{V: 1, ID: q.ID, FenceView: &hostguard.FenceView{Schema: 1, Host: q.Target.Host, Captured: captured}}, nil }) _, err := read(t.Context(), "shop.example", captured) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expired view accepted: %v", err) } }) } } type fenceViewTraceStore struct { rootstate.Store calls []string } func (s *fenceViewTraceStore) Read(name string) ([]byte, error) { s.calls = append(s.calls, "read "+name) return s.Store.Read(name) } func (s *fenceViewTraceStore) Write(name string, body []byte) error { s.calls = append(s.calls, "write "+name) return s.Store.Write(name, body) } func (s *fenceViewTraceStore) Lock(ctx context.Context, name string) (func(), error) { s.calls = append(s.calls, "lock "+name) return s.Store.Lock(ctx, name) } func (s *fenceViewTraceStore) Names() ([]string, error) { s.calls = append(s.calls, "names") return s.Store.Names() } func (s *fenceViewTraceStore) Remove(name string) error { s.calls = append(s.calls, "remove "+name) return s.Store.Remove(name) } func TestPlan14FenceViewHandlerAcquisitionSet(t *testing.T) { store := &fenceViewTraceStore{Store: &rollbackMemory{files: map[string][]byte{}}} root := hostguard.NewRootAuthority(store) obs := hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{V: 2, Host: "shop.example", Holder: &hostclaim.Holder{Account: "bab", App: "app", Incarnation: "inc", Stamp: 1}}} guard := hostguard.Guard{Root: root, Claims: stopClaimReader{obs: obs}, DataDir: t.TempDir(), LockDir: t.TempDir()} installation := hostguard.RootInstallation{Host: obs.Host, Account: "bab", App: "app", Incarnation: "inc", Stamp: 1, Migration: 1, Record: "record@etag", Box: "box", Enrollment: "enroll"} if err := guard.EstablishRoot(t.Context(), installation, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } launches, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "root.db")) if err != nil { t.Fatal(err) } defer launches.Close() journals, err := hostbox.NewJournalService(launches) if err != nil { t.Fatal(err) } guard.LockDir = filepath.Join(t.TempDir(), "absent-legacy-lock-directory") handler := ClaimRootHTTP{Journals: journals, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard}}} q := hostbox.JournalRequest{V: 1, Op: "fence-view", Target: hostguard.FenceTarget{Host: obs.Host, Incarnation: "inc", Stamp: 1}} body, err := json.Marshal(q) if err != nil { t.Fatal(err) } store.calls = nil response := httptest.NewRecorder() handler.serveJournal(response, httptest.NewRequest(http.MethodPost, "/claim/journal", bytes.NewReader(body)), StopPeer{}) if response.Code != http.StatusOK { t.Fatalf("view handler: %d %s", response.Code, response.Body) } if !reflect.DeepEqual(store.calls, []string{"lock shop.example.lock", "read shop.example.json"}) { t.Fatalf("handler acquired resources beyond its root snapshot: %v", store.calls) } var out hostbox.JournalResponse if err := json.Unmarshal(response.Body.Bytes(), &out); err != nil || out.FenceView == nil || out.FenceView.Installation != installation { t.Fatalf("view handler response: %+v %v", out, err) } } func TestPlan14FenceViewTransport(t *testing.T) { for _, attack := range []string{"none", "peer", "host", "tuple", "schema"} { t.Run(attack, func(t *testing.T) { f := newRecoveryFixture(t) o := f.read() h := o.Claim.Holder store := &rollbackDisk{dir: t.TempDir()} guard := f.guard guard.Root = hostguard.NewRootAuthority(store) in := hostguard.RootInstallation{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "box-1", Enrollment: "en", Migration: 1} if err := guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } journals, err := hostbox.NewJournalService(f.journal) if err != nil { t.Fatal(err) } before, err := store.Read(o.Host + ".json") if err != nil { t.Fatal(err) } handler := ClaimRootHTTP{Journals: journals, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard}, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { if attack == "peer" { return StopPeer{}, os.ErrPermission } return StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}, nil }}} call := rootJournalClient(startRetirementRecoveryTransport(t, handler)) host := o.Host captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} if attack == "host" { host = "other.example" } if attack == "tuple" { captured.Stamp++ } if attack == "schema" { transport := call call = func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { q.V = 99 return transport(ctx, q) } } view, err := rootFenceViewClient(call)(t.Context(), host, captured) if attack == "none" { if err != nil || view.Installation != in { t.Fatalf("authenticated view: %+v %v", view, err) } } else if err == nil { t.Fatalf("%s accepted", attack) } after, err := store.Read(o.Host + ".json") if err != nil || !bytes.Equal(before, after) { t.Fatal("read-only view wrote root state") } }) } } func TestPlan14FenceViewRefusesOldOrMalformedServer(t *testing.T) { c := hostguard.Captured{Incarnation: "inc", Stamp: 1} for _, out := range []hostbox.JournalResponse{ {V: 1}, {V: 99, FenceView: &hostguard.FenceView{Schema: 1, Host: "shop.example", Captured: c}}, {V: 1, FenceView: &hostguard.FenceView{Schema: 99, Host: "shop.example", Captured: c}}, {V: 1, FenceView: &hostguard.FenceView{Schema: 1, Host: "other.example", Captured: c}}, {V: 1, FenceView: &hostguard.FenceView{Schema: 1, Host: "shop.example", Captured: hostguard.Captured{Incarnation: "inc", Stamp: 2}}}, } { view := rootFenceViewClient(func(context.Context, hostbox.JournalRequest) (hostbox.JournalResponse, error) { return out, nil }) if _, err := view(t.Context(), "shop.example", c); err == nil { t.Fatalf("malformed view admitted: %+v", out) } } }