carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "path/filepath"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostops"
13)
14
15// useSQLInstanceSources runs the production identity reader against stubbed
16// enrollment and live-instance sources, so no test touches /etc or the
17// network.
18func useSQLInstanceSources(t *testing.T, enrolled, dmi, imds string) {
19 t.Helper()
20 restore := useSQLInstanceIdentity(readSQLInstanceIdentity)
21 oldE, oldD, oldI := sqlEnrolledInstance, sqlDMIInstance, sqlIMDSInstance
22 sqlEnrolledInstance = func() (string, error) { return enrolled, nil }
23 sqlDMIInstance = func() string { return dmi }
24 sqlIMDSInstance = func() string { return imds }
25 t.Cleanup(func() {
26 sqlEnrolledInstance, sqlDMIInstance, sqlIMDSInstance = oldE, oldD, oldI
27 restore()
28 })
29}
30
31// D1: a disk image carries root's host-actor.json with the original's
32// instance ID. Only the live machine's ID can tell a clone apart, so the
33// enrollment must match it and a copied file alone opens nothing.
34func TestPlan9Fix1SQLIdentityRefusesCopiedEnrollment(t *testing.T) {
35 path := filepath.Join(t.TempDir(), "console.db")
36 t.Run("original", func(t *testing.T) {
37 useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "i-0a1b2c3d4e5f60001", "")
38 s, err := OpenGated(path)
39 if err != nil {
40 t.Fatal(err)
41 }
42 s.Close()
43 })
44 t.Run("clone", func(t *testing.T) {
45 useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "i-0c10e0c10e0c10e01", "")
46 if s, err := OpenGated(path); err == nil {
47 s.Close()
48 t.Fatal("clone opened the original's receipt database from a copied enrollment")
49 } else if !errors.Is(err, hostops.ErrIdentity) {
50 t.Fatalf("clone refusal: %v", err)
51 }
52 })
53 t.Run("clone via IMDS", func(t *testing.T) {
54 useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "", "i-0c10e0c10e0c10e01")
55 if s, err := OpenGated(path); err == nil {
56 s.Close()
57 t.Fatal("clone opened the original's receipt database (IMDS source)")
58 }
59 })
60 t.Run("no live instance", func(t *testing.T) {
61 useSQLInstanceSources(t, "i-0a1b2c3d4e5f60001", "", "")
62 if s, err := OpenGated(filepath.Join(t.TempDir(), "fresh.db")); err == nil {
63 s.Close()
64 t.Fatal("gated DB opened on the enrollment file alone")
65 }
66 })
67}
68
69func TestPlan9Fix1SQLLiveInstanceSources(t *testing.T) {
70 for _, c := range []struct {
71 name, dmi, imds, want string
72 }{
73 {"nitro asset tag", "i-0123456789abcdef0", "i-0imds", "i-0123456789abcdef0"},
74 {"non-instance asset tag falls back", "Default string", "i-0imds", "i-0imds"},
75 {"nothing", "", "", ""},
76 } {
77 t.Run(c.name, func(t *testing.T) {
78 useSQLInstanceSources(t, "unused", c.dmi, c.imds)
79 if got := sqlLiveInstance(); got != c.want {
80 t.Fatalf("live instance = %q, want %q", got, c.want)
81 }
82 })
83 }
84}
85
86// L4: a public transaction whose own context ends is rolled back by
87// database/sql behind the callback's back. If the callback then ignores the
88// failed Commit and returns nil, the batch must still abort: its receipt
89// would otherwise claim writes that were never made.
90func TestPlan9Fix1BatchRefusesContextRolledBackTransaction(t *testing.T) {
91 s := openGatedStore(t)
92 seedGateApp(t, s)
93 r := receiptFixture(t, s)
94 err := preparedBatch(t, s, &r, func(v *Store) error {
95 ctx, cancel := context.WithCancel(context.Background())
96 defer cancel()
97 tx, err := v.db.BeginTx(ctx, nil)
98 if err != nil {
99 return err
100 }
101 if _, err = tx.Exec(`UPDATE audit SET action='ctx-lost' WHERE account_id=7 AND app='blog'`); err != nil {
102 return err
103 }
104 cancel()
105 // Wait until database/sql's own rollback reached the driver: the
106 // write is gone from this connection's view.
107 deadline := time.Now().Add(10 * time.Second)
108 for {
109 var n int
110 if err := v.db.QueryRow(`SELECT count(*) FROM audit WHERE action='ctx-lost'`).Scan(&n); err != nil {
111 return err
112 }
113 if n == 0 {
114 break
115 }
116 if time.Now().After(deadline) {
117 return errors.New("database/sql never rolled the transaction back")
118 }
119 time.Sleep(5 * time.Millisecond)
120 }
121 _ = tx.Commit() // the misuse under test: the failure is ignored
122 return nil
123 })
124 if err == nil {
125 t.Fatal("batch reported success after its transaction was rolled back by its context")
126 }
127 var n int
128 if qerr := s.rawBoot.QueryRow(`SELECT count(*) FROM claim_sql_receipts WHERE effect=? AND state='committed'`, r.Effect).Scan(&n); qerr != nil || n != 0 {
129 t.Fatalf("receipt committed for lost writes: n=%d err=%v", n, qerr)
130 }
131
132 // An explicit rollback under a live context is the callback's own choice
133 // and still lets the batch commit.
134 r2 := r
135 r2.Effect = "ingest-explicit-rollback"
136 if err = preparedBatch(t, s, &r2, func(v *Store) error {
137 tx, err := v.db.BeginTx(context.Background(), nil)
138 if err != nil {
139 return err
140 }
141 if _, err = tx.Exec(`UPDATE audit SET action='rolled' WHERE account_id=7 AND app='blog'`); err != nil {
142 tx.Rollback()
143 return err
144 }
145 return tx.Rollback()
146 }); err != nil {
147 t.Fatalf("explicit rollback aborted the batch: %v", err)
148 }
149}
150
151// L4, unbatched: the same lost transaction must not let withClaimSQL report
152// success either.
153func TestPlan9Fix1ScopeRefusesContextRolledBackTransaction(t *testing.T) {
154 s := openGatedStore(t)
155 seedGateApp(t, s)
156 err := s.withClaimSQL(t.Context(), []claimSQLScope{gateScope}, allowAllClaimSQL, func(v *Store) error {
157 ctx, cancel := context.WithCancel(context.Background())
158 tx, err := v.db.BeginTx(ctx, nil)
159 if err != nil {
160 cancel()
161 return err
162 }
163 if _, err = tx.Exec(`UPDATE audit SET action='ctx-lost' WHERE account_id=7 AND app='blog'`); err != nil {
164 cancel()
165 return err
166 }
167 cancel()
168 _ = tx.Rollback() // after its context ended: the writes are lost either way
169 return nil
170 })
171 if err == nil {
172 t.Fatal("scope reported success after losing a written transaction")
173 }
174 if countWhere(t, s, "audit", "action='ctx-lost'") != 0 {
175 t.Fatal("lost write landed")
176 }
177}
178
179// L4, failed commit: in a batch, a public commit refused because one scope
180// died must abort the batch even if the callback swallows the refusal and
181// another scope is still live.
182func TestPlan9Fix1BatchRefusesSwallowedFailedCommit(t *testing.T) {
183 s := openGatedStore(t)
184 seedGateApp(t, s)
185 r := receiptFixture(t, s)
186 other := claimSQLScope{AccountID: gateAcct, App: "blog-b", AppID: "app-B", Mode: claimModeEffect}
187 var killed atomic.Bool
188 local := func(_ context.Context, sc claimSQLScope) error {
189 if killed.Load() && sc == gateScope {
190 return errors.New("permit expired")
191 }
192 return nil
193 }
194 err := s.withPreparedClaimSQL(t.Context(), []claimSQLScope{gateScope, other}, allowAllClaimSQL, local, []hostops.SQLReceiptScope{r}, true, func(v *Store) error {
195 tx, err := v.db.Begin()
196 if err != nil {
197 return err
198 }
199 if _, err = tx.Exec(`UPDATE audit SET action='refused' WHERE account_id=7 AND app='blog'`); err != nil {
200 return err
201 }
202 killed.Store(true)
203 if err = tx.Commit(); !errors.Is(err, ErrClaimSQLDenied) {
204 return fmt.Errorf("commit err = %v, want ErrClaimSQLDenied", err)
205 }
206 return nil // the misuse under test
207 })
208 if err == nil {
209 t.Fatal("batch reported success after a swallowed failed commit")
210 }
211 var n int
212 if qerr := s.rawBoot.QueryRow(`SELECT count(*) FROM claim_sql_receipts WHERE effect=? AND state='committed'`, r.Effect).Scan(&n); qerr != nil || n != 0 {
213 t.Fatalf("receipt committed for lost writes: n=%d err=%v", n, qerr)
214 }
215}
216