carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
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
23func 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
47type fenceViewTraceStore struct {
48 rootstate.Store
49 calls []string
50}
51
52func (s *fenceViewTraceStore) Read(name string) ([]byte, error) {
53 s.calls = append(s.calls, "read "+name)
54 return s.Store.Read(name)
55}
56func (s *fenceViewTraceStore) Write(name string, body []byte) error {
57 s.calls = append(s.calls, "write "+name)
58 return s.Store.Write(name, body)
59}
60func (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}
64func (s *fenceViewTraceStore) Names() ([]string, error) {
65 s.calls = append(s.calls, "names")
66 return s.Store.Names()
67}
68func (s *fenceViewTraceStore) Remove(name string) error {
69 s.calls = append(s.calls, "remove "+name)
70 return s.Store.Remove(name)
71}
72
73func 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
113func 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
172func 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