oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "context"
5 "database/sql"
6 "database/sql/driver"
7 _ "embed"
8 "encoding/json"
9 "log/slog"
10
11 "amadan.net/rastrillo/rastrillo/migrate"
12)
13
14const (
15 legacyDiagnosticProfile = "legacy-directory-v1"
16 legacyDiagnosticMaxRows = 1000
17 legacyDiagnosticNameBytes = 256
18 legacyDiagnosticSQLBytes = 65536
19)
20
21//go:embed diagnostic_profiles/legacy-directory-v1.json
22var legacyDiagnosticJSON []byte
23
24type legacyDiagnosticObject struct{ Type, Name, Table, SQL string }
25type legacyDiagnosticResult struct {
26 status string
27 occupied bool
28}
29
30var legacyDiagnosticObjects = func() []legacyDiagnosticObject {
31 var objects []legacyDiagnosticObject
32 if err := json.Unmarshal(legacyDiagnosticJSON, &objects); err != nil {
33 panic(err)
34 }
35 return objects
36}()
37
38func legacyDiagnosticMigrations() ([]migrate.Migration, bool) {
39 return uniqueLegacyMigrations(BootSchema.All(), (Runtime{role: "directory"}).Schema().All())
40}
41
42func uniqueLegacyMigrations(groups ...[]migrate.Migration) ([]migrate.Migration, bool) {
43 seen := map[string]migrate.Migration{}
44 var result []migrate.Migration
45 for _, group := range groups {
46 for _, m := range group {
47 if m.Fn != nil || m.ID == "" || m.SQL == "" {
48 return nil, false
49 }
50 if prior, exists := seen[m.ID]; exists {
51 if prior.SQL != m.SQL {
52 return nil, false
53 }
54 continue
55 }
56 seen[m.ID] = m
57 result = append(result, m)
58 }
59 }
60 return result, len(result) > 0
61}
62
63func logLegacySchemaDiagnostic(ctx context.Context, d *sql.DB, role string, logger *slog.Logger) {
64 if role != "directory" {
65 return
66 }
67 result := inspectLegacySchema(ctx, d)
68 fields := []any{"profile", legacyDiagnosticProfile, "result", result.status}
69 if result.status == "match" {
70 fields = append(fields, "legacy_credentials_present", result.occupied)
71 }
72 logger.Info("legacy schema diagnostic", fields...)
73}
74
75func inspectLegacySchema(ctx context.Context, d *sql.DB) (result legacyDiagnosticResult) {
76 result.status = "unavailable"
77 if ctx == nil || d == nil || len(legacyDiagnosticObjects) == 0 {
78 return
79 }
80 conn, err := d.Conn(ctx)
81 if err != nil {
82 return
83 }
84 defer conn.Close()
85 if !beginLegacyDiagnostic(ctx, conn) {
86 return
87 }
88 defer func() {
89 if !rollbackLegacyDiagnostic(ctx, conn) {
90 result = legacyDiagnosticResult{status: "unavailable"}
91 }
92 }()
93 result = readLegacySchema(ctx, conn)
94 return
95}
96
97func readLegacySchema(ctx context.Context, conn *sql.Conn) legacyDiagnosticResult {
98 unavailable := legacyDiagnosticResult{status: "unavailable"}
99 mismatch := legacyDiagnosticResult{status: "mismatch"}
100 migrations, ok := legacyDiagnosticMigrations()
101 if !ok {
102 return unavailable
103 }
104 expected := map[string]string{}
105 for _, m := range migrations {
106 expected[m.ID] = migrate.Checksum(m.SQL)
107 }
108 rows, err := conn.QueryContext(ctx, `SELECT
109 CASE WHEN typeof(id)='text' AND length(CAST(id AS BLOB))<=256 THEN id END,
110 CASE WHEN typeof(checksum)='text' AND length(CAST(checksum AS BLOB))=64 THEN checksum END
111 FROM rastrillo_migrations LIMIT 1001`)
112 if err != nil {
113 return unavailable
114 }
115 matched, count := true, 0
116 for rows.Next() {
117 count++
118 var id, sum sql.NullString
119 if rows.Scan(&id, &sum) != nil {
120 rows.Close()
121 return unavailable
122 }
123 if !id.Valid || !sum.Valid || count > legacyDiagnosticMaxRows || expected[id.String] != sum.String {
124 matched = false
125 }
126 delete(expected, id.String)
127 }
128 scanErr, closeErr := rows.Err(), rows.Close()
129 if scanErr != nil || closeErr != nil || ctx.Err() != nil {
130 return unavailable
131 }
132 if !matched || len(expected) != 0 {
133 return mismatch
134 }
135 objects := map[string]legacyDiagnosticObject{}
136 for _, object := range legacyDiagnosticObjects {
137 objects[object.Type+"/"+object.Name] = object
138 }
139 rows, err = conn.QueryContext(ctx, `SELECT
140 CASE WHEN typeof(type)='text' AND length(CAST(type AS BLOB))<=16 THEN type END,
141 CASE WHEN typeof(name)='text' AND length(CAST(name AS BLOB))<=256 THEN name END,
142 CASE WHEN typeof(tbl_name)='text' AND length(CAST(tbl_name AS BLOB))<=256 THEN tbl_name END,
143 CASE WHEN typeof(coalesce(sql,''))='text' AND length(CAST(coalesce(sql,'') AS BLOB))<=65536 THEN coalesce(sql,'') END
144 FROM sqlite_schema LIMIT 1001`)
145 if err != nil {
146 return unavailable
147 }
148 count = 0
149 for rows.Next() {
150 count++
151 var kind, name, table, statement sql.NullString
152 if rows.Scan(&kind, &name, &table, &statement) != nil {
153 rows.Close()
154 return unavailable
155 }
156 object := legacyDiagnosticObject{kind.String, name.String, table.String, statement.String}
157 key := kind.String + "/" + name.String
158 prior, exists := objects[key]
159 if !kind.Valid || !name.Valid || !table.Valid || !statement.Valid || count > legacyDiagnosticMaxRows || !exists || prior != object {
160 matched = false
161 }
162 delete(objects, key)
163 }
164 scanErr, closeErr = rows.Err(), rows.Close()
165 if scanErr != nil || closeErr != nil || ctx.Err() != nil {
166 return unavailable
167 }
168 if !matched || len(objects) != 0 {
169 return mismatch
170 }
171 var occupied bool
172 // Pending challenges and recovery material count too: none is disposable here.
173 err = conn.QueryRowContext(ctx, `SELECT
174 EXISTS(SELECT 1 FROM people) OR EXISTS(SELECT 1 FROM signin_links) OR
175 EXISTS(SELECT 1 FROM passkey_credentials) OR EXISTS(SELECT 1 FROM passkey_challenges) OR
176 EXISTS(SELECT 1 FROM passkey_pending) OR EXISTS(SELECT 1 FROM passkey_recovery_codes)`).Scan(&occupied)
177 if err != nil || ctx.Err() != nil {
178 return unavailable
179 }
180 return legacyDiagnosticResult{status: "match", occupied: occupied}
181}
182
183type legacyDiagnosticConnection interface {
184 ExecContext(context.Context, string, ...any) (sql.Result, error)
185 Raw(func(any) error) error
186}
187
188func rollbackLegacyDiagnostic(ctx context.Context, conn legacyDiagnosticConnection) bool {
189 if _, err := conn.ExecContext(context.WithoutCancel(ctx), "ROLLBACK"); err != nil {
190 conn.Raw(func(any) error { return driver.ErrBadConn })
191 return false
192 }
193 return true
194}
195
196func beginLegacyDiagnostic(ctx context.Context, conn legacyDiagnosticConnection) bool {
197 if _, err := conn.ExecContext(ctx, "BEGIN"); err != nil {
198 // The driver can report cancellation after SQLite has opened the transaction.
199 conn.Raw(func(any) error { return driver.ErrBadConn })
200 return false
201 }
202 return true
203}
204