carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15type lossScopeCapture struct {
16 release.Store
17 w *lossWorld
18 state, key, id string
19 body []byte
20 etag string
21 ctx context.Context
22}
23
24func (s *lossScopeCapture) Put(ctx context.Context, key string, body []byte, etag string) (string, error) {
25 if key == s.key {
26 loss, err := hostclaim.ReadLossRecord(ctx, s.Store, s.w.pub, s.id)
27 if err == nil && loss.Record.State == s.state {
28 s.ctx = ctx
29 s.body = append([]byte(nil), body...)
30 s.etag = etag
31 return "", errors.New("captured permitted write")
32 }
33 }
34 return s.Store.Put(ctx, key, body, etag)
35}
36
37func scopedLossFixture(t *testing.T, state, kind string) (*lossWorld, *lossScopedStore, *lossScopeCapture) {
38 t.Helper()
39 w := newLossWorld(t, "landing", nil)
40 sub := w.submission(w.inspect(t, "uk-box"))
41 o := w.claim(t)
42 key := w.key
43 if kind == "claim" {
44 key = w.claimK
45 }
46 capture := &lossScopeCapture{Store: w.raw, w: w, state: state, key: key, id: sub.Confirmation}
47 cw, err := NewControlWriter(capture, w.pub, func() time.Time { return w.clock })
48 if err != nil {
49 t.Fatal(err)
50 }
51 _, err = cw.DeclareMoveLoss(t.Context(), w.signer, lossBucket, "operator", "bab", "app", lossHost, "uk-box", sub)
52 if err == nil || capture.body == nil {
53 t.Fatalf("write not captured: %v", err)
54 }
55 loss, err := hostclaim.ReadLossRecord(t.Context(), w.raw, w.pub, sub.Confirmation)
56 if err != nil || loss.Record.State != state {
57 t.Fatalf("signed declaration: %+v %v", loss, err)
58 }
59 s := &lossScopedStore{raw: w.raw, pub: w.pub, now: func() time.Time { return w.clock }, b: lossBinding{bucket: lossBucket, lossID: sub.Confirmation, host: lossHost, opID: lossOp, kind: hostclaim.KindLanding, box: "uk-box", claimETag: o.ETag, incarnation: w.inc, account: "bab", app: "app"}}
60 return w, s, capture
61}
62
63func TestJ4ScopedWritesComparePreparedAndCommittedTransitions(t *testing.T) {
64 for _, state := range []string{"prepared", "committed"} {
65 for _, kind := range []string{"claim", "record"} {
66 t.Run(state+"/"+kind, func(t *testing.T) {
67 _, s, capture := scopedLossFixture(t, state, kind)
68 if _, err := s.Put(capture.ctx, capture.key, capture.body, capture.etag); err != nil {
69 t.Fatalf("permitted write refused: %v", err)
70 }
71 for _, field := range []string{"identity", "transition", "sequence"} {
72 t.Run(field, func(t *testing.T) {
73 w, s, capture := scopedLossFixture(t, state, kind)
74 var altered map[string]any
75 dec := json.NewDecoder(strings.NewReader(string(capture.body)))
76 dec.UseNumber()
77 if err := dec.Decode(&altered); err != nil {
78 t.Fatal(err)
79 }
80 if kind == "claim" {
81 switch field {
82 case "identity":
83 altered["holder"].(map[string]any)["account"] = "other"
84 case "transition":
85 altered["op"].(map[string]any)["exec"] = "foreign"
86 case "sequence":
87 altered["gen"] = 99999
88 }
89 } else {
90 switch field {
91 case "identity":
92 altered["account"] = "other"
93 case "transition":
94 altered["move"].(map[string]any)["reason"] = "other"
95 case "sequence":
96 altered["record_seq"] = 99999
97 }
98 }
99 body, err := json.Marshal(altered)
100 if err != nil {
101 t.Fatal(err)
102 }
103 before := lossSnapshot(t, w)
104 if _, err = s.Put(capture.ctx, capture.key, body, capture.etag); !errors.Is(err, errMoveLossScope) {
105 t.Fatalf("expected scope comparison refusal, got %v", err)
106 }
107 if lossSnapshot(t, w) != before {
108 t.Fatal("altered write landed")
109 }
110 if _, err = s.Put(capture.ctx, capture.key, capture.body, capture.etag); err != nil {
111 t.Fatalf("equivalent valid write refused after negative: %v", err)
112 }
113 })
114 }
115 })
116 }
117 }
118}
119
120func TestJ4ScopedLossRecordRejectsRegressionAndEvidenceChanges(t *testing.T) {
121 for _, change := range []string{"regress", "evidence", "registration"} {
122 t.Run(change, func(t *testing.T) {
123 w, s, capture := scopedLossFixture(t, "committed", "claim")
124 loss, err := hostclaim.ReadLossRecord(t.Context(), w.raw, w.pub, capture.id)
125 if err != nil {
126 t.Fatal(err)
127 }
128 next := loss.Record
129 switch change {
130 case "regress":
131 next.State = "prepared"
132 next.ResultClaimGen = 0
133 case "evidence":
134 next.Actor = "another-operator"
135 case "registration":
136 // Removing the app admission ends the scoped transport's authority.
137 key, _ := AdmissionKey("bab", "app")
138 if err := w.raw.Delete(t.Context(), key); err != nil {
139 t.Fatal(err)
140 }
141 }
142 body, err := release.SignRootEnvelope(w.signer, next)
143 if err != nil {
144 t.Fatal(err)
145 }
146 key, _ := hostclaim.LossRecordKey(capture.id)
147 if _, err = s.Put(t.Context(), key, body, loss.ETag); err == nil {
148 t.Fatal("invalid progression forwarded")
149 }
150 })
151 }
152}
153
154func TestJ4ScopedAcceptanceRejectsOtherBucket(t *testing.T) {
155 w, s, capture := scopedLossFixture(t, "prepared", "record")
156 key := "control/host-loss-acceptances/" + capture.id + ".json"
157 raw, _, err := w.raw.Get(t.Context(), key)
158 if err != nil {
159 t.Fatal(err)
160 }
161 var a struct {
162 V int `json:"v"`
163 Bucket string `json:"bucket"`
164 RosterETag string `json:"roster_etag"`
165 Loss hostclaim.LossRecord `json:"loss"`
166 }
167 if err := release.VerifyRootEnvelope(w.pub, raw, &a); err != nil {
168 t.Fatal(err)
169 }
170 a.Bucket = "foreign"
171 body, err := release.SignRootEnvelope(w.signer, a)
172 if err != nil {
173 t.Fatal(err)
174 }
175 if err = s.acceptance(t.Context(), body, ""); !errors.Is(err, errMoveLossScope) {
176 t.Fatalf("other bucket: %v", err)
177 }
178 if err = s.acceptance(t.Context(), raw, ""); err != nil {
179 t.Fatalf("correct bucket refused: %v", err)
180 }
181}
182