| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "net/http/httptest" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "reflect" |
| 14 | "testing" |
| 15 | "time" |
| 16 | |
| 17 | "github.com/carlosframework/platform/internal/hostbox" |
| 18 | "github.com/carlosframework/platform/internal/hostclaim" |
| 19 | "github.com/carlosframework/platform/internal/hostguard" |
| 20 | "github.com/carlosframework/platform/internal/rootstate" |
| 21 | ) |
| 22 | |
| 23 | func TestPlan14FenceViewDeadline(t *testing.T) { |
| 24 | t.Parallel() |
| 25 | for _, lateSuccess := range []bool{false, true} { |
| 26 | t.Run(map[bool]string{false: "timeout", true: "late success"}[lateSuccess], func(t *testing.T) { |
| 27 | captured := hostguard.Captured{Incarnation: "inc", Stamp: 1} |
| 28 | read := rootFenceViewClient(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| 29 | deadline, ok := ctx.Deadline() |
| 30 | if !ok || time.Until(deadline) > time.Second { |
| 31 | t.Fatal("fence view has no short deadline") |
| 32 | } |
| 33 | <-ctx.Done() |
| 34 | if !lateSuccess { |
| 35 | return hostbox.JournalResponse{}, ctx.Err() |
| 36 | } |
| 37 | return hostbox.JournalResponse{V: 1, ID: q.ID, FenceView: &hostguard.FenceView{Schema: 1, Host: q.Target.Host, Captured: captured}}, nil |
| 38 | }) |
| 39 | _, err := read(t.Context(), "shop.example", captured) |
| 40 | if !errors.Is(err, context.DeadlineExceeded) { |
| 41 | t.Fatalf("expired view accepted: %v", err) |
| 42 | } |
| 43 | }) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | type fenceViewTraceStore struct { |
| 48 | rootstate.Store |
| 49 | calls []string |
| 50 | } |
| 51 | |
| 52 | func (s *fenceViewTraceStore) Read(name string) ([]byte, error) { |
| 53 | s.calls = append(s.calls, "read "+name) |
| 54 | return s.Store.Read(name) |
| 55 | } |
| 56 | func (s *fenceViewTraceStore) Write(name string, body []byte) error { |
| 57 | s.calls = append(s.calls, "write "+name) |
| 58 | return s.Store.Write(name, body) |
| 59 | } |
| 60 | func (s *fenceViewTraceStore) Lock(ctx context.Context, name string) (func(), error) { |
| 61 | s.calls = append(s.calls, "lock "+name) |
| 62 | return s.Store.Lock(ctx, name) |
| 63 | } |
| 64 | func (s *fenceViewTraceStore) Names() ([]string, error) { |
| 65 | s.calls = append(s.calls, "names") |
| 66 | return s.Store.Names() |
| 67 | } |
| 68 | func (s *fenceViewTraceStore) Remove(name string) error { |
| 69 | s.calls = append(s.calls, "remove "+name) |
| 70 | return s.Store.Remove(name) |
| 71 | } |
| 72 | |
| 73 | func TestPlan14FenceViewHandlerAcquisitionSet(t *testing.T) { |
| 74 | store := &fenceViewTraceStore{Store: &rollbackMemory{files: map[string][]byte{}}} |
| 75 | root := hostguard.NewRootAuthority(store) |
| 76 | 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}}} |
| 77 | guard := hostguard.Guard{Root: root, Claims: stopClaimReader{obs: obs}, DataDir: t.TempDir(), LockDir: t.TempDir()} |
| 78 | installation := hostguard.RootInstallation{Host: obs.Host, Account: "bab", App: "app", Incarnation: "inc", Stamp: 1, Migration: 1, Record: "record@etag", Box: "box", Enrollment: "enroll"} |
| 79 | if err := guard.EstablishRoot(t.Context(), installation, func(*hostclaim.Claim) error { return nil }); err != nil { |
| 80 | t.Fatal(err) |
| 81 | } |
| 82 | launches, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "root.db")) |
| 83 | if err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | defer launches.Close() |
| 87 | journals, err := hostbox.NewJournalService(launches) |
| 88 | if err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | guard.LockDir = filepath.Join(t.TempDir(), "absent-legacy-lock-directory") |
| 92 | handler := ClaimRootHTTP{Journals: journals, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard}}} |
| 93 | q := hostbox.JournalRequest{V: 1, Op: "fence-view", Target: hostguard.FenceTarget{Host: obs.Host, Incarnation: "inc", Stamp: 1}} |
| 94 | body, err := json.Marshal(q) |
| 95 | if err != nil { |
| 96 | t.Fatal(err) |
| 97 | } |
| 98 | store.calls = nil |
| 99 | response := httptest.NewRecorder() |
| 100 | handler.serveJournal(response, httptest.NewRequest(http.MethodPost, "/claim/journal", bytes.NewReader(body)), StopPeer{}) |
| 101 | if response.Code != http.StatusOK { |
| 102 | t.Fatalf("view handler: %d %s", response.Code, response.Body) |
| 103 | } |
| 104 | if !reflect.DeepEqual(store.calls, []string{"lock shop.example.lock", "read shop.example.json"}) { |
| 105 | t.Fatalf("handler acquired resources beyond its root snapshot: %v", store.calls) |
| 106 | } |
| 107 | var out hostbox.JournalResponse |
| 108 | if err := json.Unmarshal(response.Body.Bytes(), &out); err != nil || out.FenceView == nil || out.FenceView.Installation != installation { |
| 109 | t.Fatalf("view handler response: %+v %v", out, err) |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | func TestPlan14FenceViewTransport(t *testing.T) { |
| 114 | for _, attack := range []string{"none", "peer", "host", "tuple", "schema"} { |
| 115 | t.Run(attack, func(t *testing.T) { |
| 116 | f := newRecoveryFixture(t) |
| 117 | o := f.read() |
| 118 | h := o.Claim.Holder |
| 119 | store := &rollbackDisk{dir: t.TempDir()} |
| 120 | guard := f.guard |
| 121 | guard.Root = hostguard.NewRootAuthority(store) |
| 122 | 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} |
| 123 | if err := guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { |
| 124 | t.Fatal(err) |
| 125 | } |
| 126 | journals, err := hostbox.NewJournalService(f.journal) |
| 127 | if err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | before, err := store.Read(o.Host + ".json") |
| 131 | if err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | handler := ClaimRootHTTP{Journals: journals, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard}, Authenticate: func(context.Context, net.Conn) (StopPeer, error) { |
| 135 | if attack == "peer" { |
| 136 | return StopPeer{}, os.ErrPermission |
| 137 | } |
| 138 | return StopPeer{UID: 1001, PID: 42, ProcessStart: "start", BootID: "boot", ControllerID: "controller"}, nil |
| 139 | }}} |
| 140 | call := rootJournalClient(startRetirementRecoveryTransport(t, handler)) |
| 141 | host := o.Host |
| 142 | captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp} |
| 143 | if attack == "host" { |
| 144 | host = "other.example" |
| 145 | } |
| 146 | if attack == "tuple" { |
| 147 | captured.Stamp++ |
| 148 | } |
| 149 | if attack == "schema" { |
| 150 | transport := call |
| 151 | call = func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| 152 | q.V = 99 |
| 153 | return transport(ctx, q) |
| 154 | } |
| 155 | } |
| 156 | view, err := rootFenceViewClient(call)(t.Context(), host, captured) |
| 157 | if attack == "none" { |
| 158 | if err != nil || view.Installation != in { |
| 159 | t.Fatalf("authenticated view: %+v %v", view, err) |
| 160 | } |
| 161 | } else if err == nil { |
| 162 | t.Fatalf("%s accepted", attack) |
| 163 | } |
| 164 | after, err := store.Read(o.Host + ".json") |
| 165 | if err != nil || !bytes.Equal(before, after) { |
| 166 | t.Fatal("read-only view wrote root state") |
| 167 | } |
| 168 | }) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | func TestPlan14FenceViewRefusesOldOrMalformedServer(t *testing.T) { |
| 173 | c := hostguard.Captured{Incarnation: "inc", Stamp: 1} |
| 174 | for _, out := range []hostbox.JournalResponse{ |
| 175 | {V: 1}, |
| 176 | {V: 99, FenceView: &hostguard.FenceView{Schema: 1, Host: "shop.example", Captured: c}}, |
| 177 | {V: 1, FenceView: &hostguard.FenceView{Schema: 99, Host: "shop.example", Captured: c}}, |
| 178 | {V: 1, FenceView: &hostguard.FenceView{Schema: 1, Host: "other.example", Captured: c}}, |
| 179 | {V: 1, FenceView: &hostguard.FenceView{Schema: 1, Host: "shop.example", Captured: hostguard.Captured{Incarnation: "inc", Stamp: 2}}}, |
| 180 | } { |
| 181 | view := rootFenceViewClient(func(context.Context, hostbox.JournalRequest) (hostbox.JournalResponse, error) { return out, nil }) |
| 182 | if _, err := view(t.Context(), "shop.example", c); err == nil { |
| 183 | t.Fatalf("malformed view admitted: %+v", out) |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | |