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 "database/sql"
6 "time"
7
8 "github.com/carlosframework/platform/internal/hostops"
9)
10
11// migrateClaimAppIdentity is boot-seam schema work: it runs on the trusted,
12// unvetted boot handle, before OpenGated's census arms the gate (and is an
13// idempotent check afterwards).
14func (s *Store) migrateClaimAppIdentity() error {
15 rows, err := s.bootDB().Query(`PRAGMA table_info(apps)`)
16 if err != nil {
17 return err
18 }
19 found := map[string]bool{}
20 for rows.Next() {
21 var cid, notnull, pk int
22 var name, kind string
23 var defaultValue sql.NullString
24 if err = rows.Scan(&cid, &name, &kind, &notnull, &defaultValue, &pk); err != nil {
25 rows.Close()
26 return err
27 }
28 found[name] = true
29 }
30 err = rows.Err()
31 rows.Close()
32 if err != nil {
33 return err
34 }
35 for _, column := range []struct{ name, ddl string }{
36 {"app_id", `ALTER TABLE apps ADD COLUMN app_id TEXT NOT NULL DEFAULT ''`},
37 {"claim_op", `ALTER TABLE apps ADD COLUMN claim_op TEXT NOT NULL DEFAULT ''`},
38 {"claim_revision", `ALTER TABLE apps ADD COLUMN claim_revision INTEGER NOT NULL DEFAULT 0`},
39 } {
40 if !found[column.name] {
41 if _, err = s.bootDB().Exec(column.ddl); err != nil {
42 return err
43 }
44 }
45 }
46 for _, statement := range []string{
47 `CREATE UNIQUE INDEX IF NOT EXISTS apps_claim_identity ON apps(app_id) WHERE app_id!=''`,
48 } {
49 if _, err = s.bootDB().Exec(statement); err != nil {
50 return err
51 }
52 }
53 return nil
54}
55
56// ClaimAppID is the row's bound incarnation. sql.ErrNoRows when there is no
57// row; hostops.ErrIdentity when the row is not bound yet.
58func (s *Store) ClaimAppID(accountID int64, app string) (string, error) {
59 var id string
60 err := s.db.QueryRow(`SELECT app_id FROM apps WHERE account_id=? AND app=?`, accountID, app).Scan(&id)
61 if err == nil && id == "" {
62 return "", hostops.ErrIdentity
63 }
64 return id, err
65}
66
67func (s *Store) BindClaimAppOperation(accountID int64, app, appID, op string, revision uint64) error {
68 result, err := s.db.Exec(`UPDATE apps SET claim_op=?,claim_revision=? WHERE account_id=? AND app=? AND app_id=? AND (claim_revision<? OR (claim_revision=? AND claim_op=?))`,
69 op, revision, accountID, app, appID, revision, revision, op)
70 if err != nil {
71 return err
72 }
73 n, err := result.RowsAffected()
74 if err != nil {
75 return err
76 }
77 if n != 1 {
78 return hostops.ErrPending
79 }
80 return nil
81}
82
83func (s *Store) SetClaimAppPlacement(accountID int64, app, appID, op, target string) error {
84 result, err := s.db.Exec(`UPDATE apps SET placement=? WHERE account_id=? AND app=? AND app_id=? AND claim_op=?`, target, accountID, app, appID, op)
85 if err != nil {
86 return err
87 }
88 n, err := result.RowsAffected()
89 if err != nil {
90 return err
91 }
92 if n != 1 {
93 return hostops.ErrPending
94 }
95 return nil
96}
97
98// TrashClaimAppRow is TrashAppRow bound to the incarnation: the marker's
99// clock, the temporary row's early-purge window, first trash wins.
100func (s *Store) TrashClaimAppRow(accountID int64, app, appID string, at time.Time) (bool, error) {
101 sec := at.Unix()
102 return s.changed(`UPDATE apps SET deleted_at = ?,
103 purge_for = CASE WHEN expires_at > 0 THEN ? ELSE 0 END,
104 purge_after = CASE WHEN expires_at > 0 THEN ? ELSE 0 END
105WHERE account_id = ? AND app = ? AND app_id = ? AND deleted_at = 0`,
106 sec, sec, sec+temporaryCooloffSeconds, accountID, app, appID)
107}
108
109// SetClaimRestoring is SetRestoring bound to the incarnation.
110func (s *Store) SetClaimRestoring(accountID int64, app, appID string, on bool) (bool, error) {
111 if on {
112 return s.changed(`UPDATE apps SET restoring = 1 WHERE account_id = ? AND app = ? AND app_id = ? AND deleted_at != 0`, accountID, app, appID)
113 }
114 return s.changed(`UPDATE apps SET restoring = 0 WHERE account_id = ? AND app = ? AND app_id = ? AND restoring != 0`, accountID, app, appID)
115}
116
117// BumpClaimTrashedExpiry is BumpTrashedExpiry bound to the incarnation.
118func (s *Store) BumpClaimTrashedExpiry(accountID int64, app, appID string, now time.Time) (bool, error) {
119 return s.changed(`UPDATE apps SET expires_at = ?
120WHERE account_id = ? AND app = ? AND app_id = ? AND expires_at > 0 AND expires_at < ? AND deleted_at != 0`,
121 now.Add(temporaryTTLDefault).Unix(), accountID, app, appID, now.Add(temporaryRestoreFloor).Unix())
122}
123
124// FinishClaimRestoreRow is FinishRestoreRow (restore step 5, with the
125// temporary app's expiry bump folded in as projection) bound to the
126// incarnation and to the receipt: only a row trashed by exactly the receipt's
127// marker (deleted_at = the marker's clock) is restored. A row trashed by any
128// other trash keeps its trash.
129func (s *Store) FinishClaimRestoreRow(accountID int64, app, appID string, receiptDeletedAt int64, now time.Time) (bool, error) {
130 if receiptDeletedAt <= 0 {
131 return false, hostops.ErrIdentity
132 }
133 return s.changed(`UPDATE apps SET deleted_at = 0, restoring = 0, purge_after = 0, purge_for = 0,
134 expires_at = CASE WHEN expires_at > 0 AND expires_at < ? THEN ? ELSE expires_at END
135WHERE account_id = ? AND app = ? AND app_id = ? AND deleted_at = ?`,
136 now.Add(temporaryRestoreFloor).Unix(), now.Add(temporaryTTLDefault).Unix(), accountID, app, appID, receiptDeletedAt)
137}
138
139// MarkClaimAppRetired hides a row whose incarnation a completed purge
140// retired, so PurgeClaimAppRow's deleted_at != 0 guard holds on a console
141// that never saw the marker.
142func (s *Store) MarkClaimAppRetired(accountID int64, app, appID string, now time.Time) error {
143 _, err := s.db.Exec(`UPDATE apps SET deleted_at=? WHERE account_id=? AND app=? AND app_id=? AND deleted_at=0`, now.Unix(), accountID, app, appID)
144 return err
145}
146
147func (s *Store) PurgeClaimAppRow(ctx context.Context, accountID int64, app, appID, op string) error {
148 tx, err := s.db.BeginTx(ctx, nil)
149 if err != nil {
150 return err
151 }
152 defer tx.Rollback()
153 var current string
154 err = tx.QueryRowContext(ctx, `SELECT app_id FROM apps WHERE account_id=? AND app=?`, accountID, app).Scan(&current)
155 if err == sql.ErrNoRows {
156 return tx.Commit()
157 }
158 if err != nil {
159 return err
160 }
161 if current != appID {
162 return hostops.ErrIdentity
163 }
164 res, err := tx.ExecContext(ctx, `DELETE FROM apps WHERE account_id=? AND app=? AND app_id=? AND claim_op=? AND deleted_at!=0`, accountID, app, appID, op)
165 if err != nil {
166 return err
167 }
168 n, err := res.RowsAffected()
169 if err != nil {
170 return err
171 }
172 if n != 1 {
173 return hostops.ErrPending
174 }
175 var sqid string
176 if err = tx.QueryRowContext(ctx, `SELECT sqid FROM accounts WHERE id=?`, accountID).Scan(&sqid); err != nil {
177 return err
178 }
179 if err = forgetMailApp(ctx, tx, sqid, app); err != nil {
180 return err
181 }
182 if _, err = tx.ExecContext(ctx, `DELETE FROM cli_tokens WHERE kind IN (?,?) AND account_id=? AND app=?`, cliKindApp, cliKindService, accountID, app); err != nil {
183 return err
184 }
185 if err = revokeClaimDoorKeys(tx, accountID, app); err != nil {
186 return err
187 }
188 return tx.Commit()
189}
190
191// revokeClaimDoorKeys is revokeAllDoorKeys for the claim purge. A purge
192// permit may only raise door_keys.revoked_at and door_key_revisions.revision
193// (never insert), so the revision moves by a plain UPDATE: an app that has
194// keys always has its revision row, written in the same transaction as the
195// key (DoorKeyInsert). The revision still climbs past what boxes admitted.
196func revokeClaimDoorKeys(tx *sql.Tx, accountID int64, app string) error {
197 res, err := tx.Exec(`UPDATE door_keys SET revoked_at = ? WHERE account_id = ? AND app = ? AND revoked_at = 0`,
198 time.Now().UnixNano(), accountID, app)
199 if err != nil {
200 return err
201 }
202 if n, err := res.RowsAffected(); err != nil || n == 0 {
203 return err
204 }
205 _, err = tx.Exec(`UPDATE door_key_revisions SET revision = revision + 1 WHERE account_id = ? AND app = ?`, accountID, app)
206 return err
207}
208