oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package home
4
5import (
6 "bytes"
7 "context"
8 "crypto/sha256"
9 "database/sql"
10 "encoding/hex"
11 "encoding/json"
12 "go/ast"
13 "go/parser"
14 "go/token"
15 "io/fs"
16 "os"
17 "path/filepath"
18 "sort"
19 "strings"
20 "testing"
21
22 "amadan.net/rastrillo/rastrillo/migrate"
23)
24
25func TestSchemaCommitmentFrozenCatalog(t *testing.T) {
26 for _, id := range []string{"credential-current-v1", "directory-current-v1", "directory-historical-empty-v1"} {
27 t.Run(id, func(t *testing.T) {
28 role := strings.Split(id, "-")[0]
29 migrations := compatibilitySchema(role).All()
30 objectsRaw, err := compatibilityProfiles.ReadFile("compatibility_profiles/" + role + ".json")
31 if err != nil {
32 t.Fatal(err)
33 }
34 empty := []string{}
35 if id == "directory-historical-empty-v1" {
36 objectsRaw = legacyDiagnosticJSON
37 var ok bool
38 migrations, ok = legacyDiagnosticMigrations()
39 if !ok {
40 t.Fatal("historical migration conflict")
41 }
42 empty = []string{"passkey_challenges", "passkey_credentials", "passkey_pending", "passkey_recovery_codes", "people", "signin_links"}
43 }
44 m := schemaCommitmentManifest{Version: 1, ID: id, Role: role, ValidationPolicy: 1, EmptyTables: empty, Migrations: []schemaCommitmentMigration{}}
45 if json.Unmarshal(objectsRaw, &m.Objects) != nil {
46 t.Fatal("frozen objects")
47 }
48 for _, migration := range migrations {
49 if migration.Fn != nil {
50 t.Fatal("Go migration cannot be committed")
51 }
52 m.Migrations = append(m.Migrations, schemaCommitmentMigration{migration.ID, migrate.Checksum(migration.SQL)})
53 }
54 sort.Slice(m.Migrations, func(i, j int) bool { return m.Migrations[i].ID < m.Migrations[j].ID })
55 sort.Slice(m.Objects, func(i, j int) bool {
56 a, b := m.Objects[i], m.Objects[j]
57 return a.Type < b.Type || a.Type == b.Type && a.Name < b.Name
58 })
59 raw, err := json.MarshalIndent(m, "", " ")
60 if err != nil {
61 t.Fatal(err)
62 }
63 raw = append(raw, '\n')
64 name := "admission_profiles/" + id + ".json"
65 if os.Getenv("OFICINA_GENERATE_ADMISSION_PROFILE") == "1" {
66 if err := os.WriteFile(name, raw, 0600); err != nil {
67 t.Fatal(err)
68 }
69 return
70 }
71 frozen, err := admissionProfiles.ReadFile(name)
72 if err != nil || !bytes.Equal(raw, frozen) {
73 t.Fatal("admission manifest differs from frozen schema/provenance")
74 }
75 if _, err := decodeSchemaCommitment(frozen); err != nil {
76 t.Fatal(err)
77 }
78 })
79 }
80}
81
82func selectedSchema(t *testing.T, id string) schemaCommitment {
83 t.Helper()
84 raw, err := admissionProfiles.ReadFile("admission_profiles/" + id + ".json")
85 if err != nil {
86 t.Fatal(err)
87 }
88 sum := sha256.Sum256(raw)
89 selected, err := loadSchemaCommitment(strings.Split(id, "-")[0], id, hex.EncodeToString(sum[:]))
90 if err != nil {
91 t.Fatal(err)
92 }
93 return selected
94}
95
96func TestSchemaCommitmentRefusesWireAndSelectionChanges(t *testing.T) {
97 selected := selectedSchema(t, "directory-historical-empty-v1")
98 raw, _ := admissionProfiles.ReadFile("admission_profiles/directory-historical-empty-v1.json")
99 for name, changed := range map[string][]byte{
100 "newline": bytes.TrimSuffix(raw, []byte{'\n'}),
101 "whitespace": append([]byte{' '}, raw...),
102 "duplicate": bytes.Replace(raw, []byte(`"version": 1,`), []byte("\"version\": 1,\n \"version\": 1,"), 1),
103 "unknown": bytes.Replace(raw, []byte(`"version": 1,`), []byte(`"unknown": 1, "version": 1,`), 1),
104 "wrong-policy": bytes.Replace(raw, []byte(`"validation_policy": 1`), []byte(`"validation_policy": 2`), 1),
105 "trailing-json": append(append([]byte{}, raw...), []byte("{}\n")...),
106 } {
107 t.Run(name, func(t *testing.T) {
108 if _, err := decodeSchemaCommitment(changed); err == nil {
109 t.Fatal("accepted altered wire")
110 }
111 })
112 }
113 for _, input := range [][3]string{
114 {"credential", selected.manifest.ID, selected.digest},
115 {"directory", "../directory-historical-empty-v1", selected.digest},
116 {"directory", selected.manifest.ID, strings.Repeat("0", 64)},
117 {"directory", "", selected.digest}, {"directory", selected.manifest.ID, ""},
118 } {
119 if _, err := loadSchemaCommitment(input[0], input[1], input[2]); err == nil {
120 t.Fatal("accepted altered selection")
121 }
122 }
123}
124
125func checkSelectedSchema(t *testing.T, d *sql.DB, selected schemaCommitment) error {
126 t.Helper()
127 conn, err := d.Conn(t.Context())
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer conn.Close()
132 if _, err := conn.ExecContext(t.Context(), "BEGIN IMMEDIATE"); err != nil {
133 t.Fatal(err)
134 }
135 result := checkSchemaCommitment(t.Context(), conn, selected)
136 if _, err := conn.ExecContext(t.Context(), "ROLLBACK"); err != nil {
137 t.Fatal(err)
138 }
139 return result
140}
141
142func TestSchemaCommitmentReplayAndOccupancy(t *testing.T) {
143 for _, role := range []string{"credential", "directory"} {
144 d := compatibilityFixture(t, role)
145 if err := checkSelectedSchema(t, d, selectedSchema(t, role+"-current-v1")); err != nil {
146 t.Fatal(err)
147 }
148 d.Close()
149 }
150 selected := selectedSchema(t, "directory-historical-empty-v1")
151 for name, change := range map[string]string{
152 "valid": "",
153 "people": "INSERT INTO people VALUES('person','index','time')",
154 "signin_links": "INSERT INTO signin_links VALUES('token',x'00','purpose','time')",
155 "passkey_credentials": "INSERT INTO passkey_credentials VALUES('id','person',x'00',0,'time')",
156 "passkey_challenges": "INSERT INTO passkey_challenges VALUES('challenge','person','purpose','time')",
157 "passkey_pending": "INSERT INTO passkey_pending VALUES('token','person','method','/','time')",
158 "passkey_recovery_codes": "INSERT INTO passkey_recovery_codes VALUES('code','person','time')",
159 "checksum": "UPDATE rastrillo_migrations SET checksum='wrong'",
160 "missing-migration": "DELETE FROM rastrillo_migrations WHERE id=(SELECT id FROM rastrillo_migrations LIMIT 1)",
161 "extra-table": "CREATE TABLE unexpected(value TEXT)",
162 "extra-trigger": "CREATE TRIGGER unexpected AFTER DELETE ON people BEGIN SELECT 1; END",
163 "replication-lock": "INSERT INTO _litestream_lock VALUES(1)",
164 "replication-seq": "INSERT INTO _litestream_seq VALUES(2,1)",
165 "oversized-id": "UPDATE rastrillo_migrations SET id=printf('%300s',id) WHERE id=(SELECT id FROM rastrillo_migrations LIMIT 1)",
166 } {
167 t.Run(name, func(t *testing.T) {
168 d := legacyDiagnosticFixture(t, true, true)
169 if err := checkSelectedSchema(t, d.Writer(), selected); err != nil {
170 t.Fatal("baseline", err)
171 }
172 if change != "" {
173 if _, err := d.Writer().Exec(change); err != nil {
174 t.Fatal(err)
175 }
176 }
177 err := checkSelectedSchema(t, d.Writer(), selected)
178 if (name == "valid") != (err == nil) {
179 t.Fatalf("check %s: %v", name, err)
180 }
181 })
182 }
183 d := legacyDiagnosticFixture(t, true, true)
184 if err := checkSelectedSchema(t, d.Writer(), selectedSchema(t, "directory-current-v1")); err == nil {
185 t.Fatal("fresh profile accepted historical database")
186 }
187 fresh := compatibilityFixture(t, "directory")
188 defer fresh.Close()
189 if err := checkSelectedSchema(t, fresh, selected); err == nil {
190 t.Fatal("historical profile accepted fresh database")
191 }
192}
193
194func TestSchemaCommitmentHasNoProductionCaller(t *testing.T) {
195 // Every reference outside the admission implementation is forbidden. This is
196 // stronger than following the current command, journal and checker roots.
197 root := "../.."
198 fileSet := token.NewFileSet()
199 definitions := map[string]bool{}
200 allowed := map[string]bool{}
201 for _, path := range []string{"internal/home/schema_commitment_linux.go", "internal/operator/schema_protocol.go", "internal/operator/schema_command_linux.go", "internal/provisioner/schema_plan_linux.go"} {
202 full := filepath.Clean(filepath.Join(root, path))
203 allowed[full] = true
204 source, err := parser.ParseFile(fileSet, full, nil, 0)
205 if err != nil {
206 t.Fatal(err)
207 }
208 for _, decl := range source.Decls {
209 switch d := decl.(type) {
210 case *ast.FuncDecl:
211 definitions[d.Name.Name] = true
212 case *ast.GenDecl:
213 for _, spec := range d.Specs {
214 switch s := spec.(type) {
215 case *ast.TypeSpec:
216 definitions[s.Name.Name] = true
217 case *ast.ValueSpec:
218 for _, n := range s.Names {
219 definitions[n.Name] = true
220 }
221 }
222 }
223 }
224 }
225 }
226 err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
227 if err != nil {
228 return err
229 }
230 if d.IsDir() {
231 return nil
232 }
233 if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") || allowed[filepath.Clean(path)] {
234 return nil
235 }
236 f, err := parser.ParseFile(fileSet, path, nil, 0)
237 if err != nil {
238 return err
239 }
240 ast.Inspect(f, func(n ast.Node) bool {
241 if id, ok := n.(*ast.Ident); ok && definitions[id.Name] {
242 t.Errorf("unmounted admission reference %s at %s", id.Name, fileSet.Position(id.Pos()))
243 }
244 return true
245 })
246 return nil
247 })
248 if err != nil {
249 t.Fatal(err)
250 }
251}
252
253func TestSchemaCommitmentGoldenDigests(t *testing.T) {
254 for id, want := range map[string]string{
255 "credential-current-v1": "db65d14c48ef762b692d8a74f158e46e5d6117518092274dcefe6a1319d01451",
256 "directory-current-v1": "344fca53f2cfc5eae5f1c05eb650c503c8af14cc98ec410b3146cfee8eebae02",
257 "directory-historical-empty-v1": "1313fb93488e25164ed127927725194dc07c524be6219beedc162b4ce3588309",
258 } {
259 if selectedSchema(t, id).digest != want {
260 t.Fatal("immutable manifest changed", id)
261 }
262 }
263}
264
265func TestSchemaCommitmentPreservesSessionsAndRejectsLaterOccupancy(t *testing.T) {
266 d := legacyDiagnosticFixture(t, true, true)
267 selected := selectedSchema(t, "directory-historical-empty-v1")
268 if _, err := d.Writer().Exec("INSERT INTO sessions VALUES('hash','opaque-person','method','auth-time','created','expires')"); err != nil {
269 t.Fatal(err)
270 }
271 if err := checkSelectedSchema(t, d.Writer(), selected); err != nil {
272 t.Fatal("shared session refused", err)
273 }
274 var subject string
275 if err := d.Writer().QueryRow("SELECT subject FROM sessions WHERE token_hash='hash'").Scan(&subject); err != nil || subject != "opaque-person" {
276 t.Fatal("session changed")
277 }
278 if _, err := d.Writer().Exec("INSERT INTO people VALUES('later','later','time')"); err != nil {
279 t.Fatal(err)
280 }
281 if err := checkSelectedSchema(t, d.Writer(), selected); err == nil {
282 t.Fatal("earlier successful check authorized later occupancy")
283 }
284 conn, err := d.Writer().Conn(t.Context())
285 if err != nil {
286 t.Fatal(err)
287 }
288 defer conn.Close()
289 ctx, cancel := context.WithCancel(t.Context())
290 cancel()
291 if err := checkSchemaCommitment(ctx, conn, selected); err == nil {
292 t.Fatal("cancelled check accepted")
293 }
294}
295