Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Identify accounts by their registered name

62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 15d ago · parent 642e5994

Identify accounts by their registered name

Drop the internal account id and display name. Users, keys, passkeys,
invites, sessions, ACL entries, approvals, locks and CI records now all
use the registered name as the single identity.

The roster migrates in place: old id-keyed tables are rebuilt around the
name, and access-rule/lock references are rewritten through the same
mapping. Display-name editing and whoami --id are removed.

forge/identity/identity.go +425−266

@@ -4,27 +4,24 @@
4 4 // the CI queue (runs.db). A roster is small and written by a handful of operator
5 5 // commands, so the whole thing is a handful of rows:
6 6 //
7 // users one row per account: stable id, fixed name, display, status, created
7 // users one row per account: fixed name, status, created
8 8 // user_keys one row per authorized SSH key, unique by fingerprint
9 9 // user_credentials one row per passkey: the browser credential's public half
10 10 // user_invites one-time enrollment tokens, stored only as hashes
11 11 // sessions browser sessions, stored only as token hashes
12 12 //
13 // An account has a stable id and a name that is fixed the moment it is created.
14 // The name is what contributor branches and metadata authors are written in, so
15 // it can never move: a rename would strand every ref under the old folder. The
16 // id is what ACL entries and approval files are keyed by. Because the users row
17 // is kept after retirement, the UNIQUE constraint on the name stops a retired
18 // name being handed to a second account — which is what stops a new user from
19 // inheriting an old one's access and approvals.
20 //
21 // A display name is optional and mutable. It is a label for people, never an
22 // identity: nothing is authorized by it and nothing on disk is keyed by it.
13 // An account is identified by its registered name, which is fixed the moment it
14 // is created. The name is what contributor branches and metadata authors are
15 // written in, so it can never move: a rename would strand every ref under the old
16 // folder. It is also what ACL entries, approvals, locks and CI records are keyed
17 // by. Because the users row is kept after retirement, the UNIQUE constraint on the
18 // name stops a retired name being handed to a second account — which is what stops
19 // a new user from inheriting an old one's access and approvals.
23 20 //
24 21 // The credentials anchor the account. An SSH key is resolved at the SSH
25 22 // handshake; a passkey is resolved in the browser login ceremony. Both yield the
26 // same account id, and every authorization decision downstream uses that id
27 // rather than a name somebody chose to type.
23 // same account name, and every authorization decision downstream uses that name
24 // rather than a second internal identifier.
28 25 //
29 26 // Nothing stored here is a usable secret: public keys and public credential
30 27 // halves, plus hashes of session and invite tokens. The database file can still
@@ -36,6 +33,7 @@ package identity
36 33
37 34 import (
38 35 "database/sql"
36 "encoding/json"
39 37 "errors"
40 38 "fmt"
41 39 "os"
@@ -45,12 +43,9 @@ import (
45 43 "strings"
46 44 "sync/atomic"
47 45 "time"
48 "unicode"
49 "unicode/utf8"
50 46
51 47 _ "modernc.org/sqlite"
52 48
53 "github.com/rodent-software/vole/core/id"
54 49 "golang.org/x/crypto/ssh"
55 50 )
56 51
@@ -66,30 +61,6 @@ func ValidateName(s string) error {
66 61 return nil
67 62 }
68 63
69 // maxDisplayRunes bounds a display name. It is a label, not an identity, so the
70 // limit only has to keep a page or a table readable.
71 const maxDisplayRunes = 64
72
73 // ValidateDisplay reports whether s may be a display name. Anything printable is
74 // allowed, including spaces, capitals and non-Latin scripts; control characters
75 // are not, because a newline or tab would break the one-line listings a display
76 // name shows up in.
77 func ValidateDisplay(s string) error {
78 s = strings.TrimSpace(s)
79 if s == "" {
80 return nil
81 }
82 if n := utf8.RuneCountInString(s); n > maxDisplayRunes {
83 return fmt.Errorf("display name is %d characters; the limit is %d", n, maxDisplayRunes)
84 }
85 for _, r := range s {
86 if unicode.IsControl(r) {
87 return fmt.Errorf("display name %q contains a control character", s)
88 }
89 }
90 return nil
91 }
92
93 64 type Key struct {
94 65 Fingerprint string `json:"fingerprint"`
95 66 Type string `json:"type"`
@@ -98,9 +69,7 @@ type Key struct {
98 69 }
99 70
100 71 type Account struct {
101 ID string
102 72 Name string // fixed at creation; contributor branches are named after it
103 Display string // optional human-friendly label; may change
104 73 Status string // active | retired
105 74 Keys []Key
106 75 Admin bool
@@ -109,30 +78,17 @@ type Account struct {
109 78
110 79 func (a *Account) Active() bool { return a.Status == "active" }
111 80
112 // Label is what a person is shown: the display name when one is set, the
113 // immutable name otherwise.
114 func (a *Account) Label() string {
115 if a == nil {
116 return ""
117 }
118 if a.Display != "" {
119 return a.Display
120 }
121 return a.Name
122 }
123
124 81 // Snapshot is an immutable view of the roster, swapped atomically so the SSH
125 82 // auth callback stays a map lookup.
126 83 type Snapshot struct {
127 ByID map[string]*Account
128 // ByKey maps a key fingerprint to an active account id. Retired accounts are
129 // absent, so revoking is immediate.
84 // ByKey maps a key fingerprint to an active account name. Retired accounts
85 // are absent, so revoking is immediate.
130 86 ByKey map[string]string
131 87 // ByName maps a name to its account, including retired accounts: a name is
132 88 // fixed at creation and remains attached to the account that took it.
133 89 ByName map[string]*Account
134 90
135 // Admins holds the account ids that may use the remote administration API.
91 // Admins holds the account names that may use the remote administration API.
136 92 Admins map[string]bool
137 93
138 94 Count int // active accounts
@@ -142,8 +98,8 @@ func (s *Snapshot) Lookup(fingerprint string) (string, bool) {
142 98 if s == nil {
143 99 return "", false
144 100 }
145 a, ok := s.ByKey[fingerprint]
146 return a, ok
101 name, ok := s.ByKey[fingerprint]
102 return name, ok
147 103 }
148 104
149 105 type Store struct {
@@ -156,26 +112,24 @@ type Store struct {
156 112
157 113 const schema = `
158 114 CREATE TABLE IF NOT EXISTS users (
159 id TEXT PRIMARY KEY,
160 name TEXT NOT NULL UNIQUE,
161 display TEXT NOT NULL DEFAULT '',
115 name TEXT PRIMARY KEY,
162 116 status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','retired')),
163 117 created INTEGER NOT NULL
164 118 );
165 119 CREATE TABLE IF NOT EXISTS user_keys (
166 120 fingerprint TEXT PRIMARY KEY,
167 user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
121 user_name TEXT NOT NULL REFERENCES users(name) ON DELETE CASCADE,
168 122 type TEXT NOT NULL,
169 123 line TEXT NOT NULL,
170 124 comment TEXT NOT NULL DEFAULT ''
171 125 );
172 CREATE INDEX IF NOT EXISTS user_keys_owner ON user_keys(user_id);
126 CREATE INDEX IF NOT EXISTS user_keys_owner ON user_keys(user_name);
173 127 CREATE TABLE IF NOT EXISTS instance_admins (
174 user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE
128 user_name TEXT PRIMARY KEY REFERENCES users(name) ON DELETE CASCADE
175 129 );
176 130 CREATE TABLE IF NOT EXISTS user_credentials (
177 131 credential_id TEXT PRIMARY KEY,
178 user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
132 user_name TEXT NOT NULL REFERENCES users(name) ON DELETE CASCADE,
179 133 public_key BLOB NOT NULL,
180 134 attestation_type TEXT NOT NULL DEFAULT '',
181 135 transports TEXT NOT NULL DEFAULT '[]',
@@ -188,26 +142,26 @@ CREATE TABLE IF NOT EXISTS user_credentials (
188 142 created INTEGER NOT NULL,
189 143 last_used INTEGER NOT NULL DEFAULT 0
190 144 );
191 CREATE INDEX IF NOT EXISTS user_credentials_owner ON user_credentials(user_id);
145 CREATE INDEX IF NOT EXISTS user_credentials_owner ON user_credentials(user_name);
192 146 CREATE TABLE IF NOT EXISTS user_invites (
193 147 token_hash TEXT PRIMARY KEY,
194 user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
148 user_name TEXT NOT NULL REFERENCES users(name) ON DELETE CASCADE,
195 149 created INTEGER NOT NULL,
196 150 expires INTEGER NOT NULL,
197 151 used_at INTEGER,
198 152 created_by TEXT NOT NULL DEFAULT ''
199 153 );
200 CREATE INDEX IF NOT EXISTS user_invites_owner ON user_invites(user_id);
154 CREATE INDEX IF NOT EXISTS user_invites_owner ON user_invites(user_name);
201 155 CREATE TABLE IF NOT EXISTS sessions (
202 156 token_hash TEXT PRIMARY KEY,
203 account_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
157 user_name TEXT NOT NULL REFERENCES users(name) ON DELETE CASCADE,
204 158 credential_id TEXT REFERENCES user_credentials(credential_id) ON DELETE CASCADE,
205 159 created INTEGER NOT NULL,
206 160 last_seen INTEGER NOT NULL,
207 161 expires INTEGER NOT NULL,
208 162 user_agent TEXT NOT NULL DEFAULT ''
209 163 );
210 CREATE INDEX IF NOT EXISTS sessions_account ON sessions(account_id);
164 CREATE INDEX IF NOT EXISTS sessions_user ON sessions(user_name);
211 165 CREATE INDEX IF NOT EXISTS sessions_expiry ON sessions(expires);
212 166 `
213 167
@@ -234,30 +188,327 @@ func Init(path string) error {
234 188 return fmt.Errorf("identity: %w", err)
235 189 }
236 190 defer db.Close()
237 if _, err := db.Exec(schema); err != nil {
238 return fmt.Errorf("identity: create roster: %w", err)
239 }
240 191 if err := migrate(db); err != nil {
241 192 return fmt.Errorf("identity: migrate roster: %w", err)
242 193 }
194 if _, err := db.Exec(schema); err != nil {
195 return fmt.Errorf("identity: create roster: %w", err)
196 }
243 197 return nil
244 198 }
245 199
246 200 // migrate upgrades a roster written by an older build in place. It is idempotent
247 201 // and runs on every open, so an operator who restores an old backup does not
248 202 // have to remember which version produced it.
203 //
204 // Rosters used to key accounts by an internal id and carry a display label.
205 // Both are gone: the registered name is the identity. A roster with the old
206 // shape is rebuilt around the name, copying every row through the account it
207 // belonged to, so no key, passkey, invite, session or admin grant is lost.
249 208 func migrate(db *sql.DB) error {
250 // Display names were added after the first release. A roster without the
251 // column is upgraded rather than rejected; a roster that already has it keeps
252 // its values.
253 if _, err := db.Exec(`ALTER TABLE users ADD COLUMN display TEXT NOT NULL DEFAULT ''`); err != nil && !strings.Contains(err.Error(), "duplicate column name") {
209 hasUsers, err := tableExists(db, "users")
210 if err != nil {
211 return err
212 }
213 if !hasUsers {
214 return nil
215 }
216 hasID, err := columnExists(db, "users", "id")
217 if err != nil {
218 return err
219 }
220 if !hasID {
221 // A name-keyed roster is current. The alias table from an even older
222 // build is redundant: the users row itself reserves the name.
223 _, err := db.Exec(`DROP TABLE IF EXISTS user_names`)
224 return err
225 }
226
227 tx, err := db.Begin()
228 if err != nil {
229 return err
230 }
231 defer tx.Rollback()
232
233 // Build the replacement tables first, then copy through the old id so a
234 // reference becomes the name it resolved to.
235 for _, name := range []string{"users_new", "user_keys_new", "instance_admins_new",
236 "user_credentials_new", "user_invites_new", "sessions_new"} {
237 if _, err := tx.Exec(`DROP TABLE IF EXISTS ` + name); err != nil {
238 return err
239 }
240 }
241 if _, err := tx.Exec(`
242 CREATE TABLE users_new (
243 name TEXT PRIMARY KEY,
244 status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','retired')),
245 created INTEGER NOT NULL
246 );
247 CREATE TABLE user_keys_new (
248 fingerprint TEXT PRIMARY KEY,
249 user_name TEXT NOT NULL REFERENCES users_new(name) ON DELETE CASCADE,
250 type TEXT NOT NULL,
251 line TEXT NOT NULL,
252 comment TEXT NOT NULL DEFAULT ''
253 );
254 CREATE TABLE instance_admins_new (
255 user_name TEXT PRIMARY KEY REFERENCES users_new(name) ON DELETE CASCADE
256 );
257 CREATE TABLE user_credentials_new (
258 credential_id TEXT PRIMARY KEY,
259 user_name TEXT NOT NULL REFERENCES users_new(name) ON DELETE CASCADE,
260 public_key BLOB NOT NULL,
261 attestation_type TEXT NOT NULL DEFAULT '',
262 transports TEXT NOT NULL DEFAULT '[]',
263 flags INTEGER NOT NULL DEFAULT 0,
264 aaguid BLOB,
265 sign_count INTEGER NOT NULL DEFAULT 0,
266 clone_warning INTEGER NOT NULL DEFAULT 0,
267 attachment TEXT NOT NULL DEFAULT '',
268 label TEXT NOT NULL DEFAULT '',
269 created INTEGER NOT NULL,
270 last_used INTEGER NOT NULL DEFAULT 0
271 );
272 CREATE TABLE user_invites_new (
273 token_hash TEXT PRIMARY KEY,
274 user_name TEXT NOT NULL REFERENCES users_new(name) ON DELETE CASCADE,
275 created INTEGER NOT NULL,
276 expires INTEGER NOT NULL,
277 used_at INTEGER,
278 created_by TEXT NOT NULL DEFAULT ''
279 );
280 CREATE TABLE sessions_new (
281 token_hash TEXT PRIMARY KEY,
282 user_name TEXT NOT NULL REFERENCES users_new(name) ON DELETE CASCADE,
283 credential_id TEXT REFERENCES user_credentials_new(credential_id) ON DELETE CASCADE,
284 created INTEGER NOT NULL,
285 last_seen INTEGER NOT NULL,
286 expires INTEGER NOT NULL,
287 user_agent TEXT NOT NULL DEFAULT ''
288 );`); err != nil {
289 return err
290 }
291
292 if _, err := tx.Exec(`INSERT INTO users_new(name, status, created) SELECT name, status, created FROM users`); err != nil {
293 return err
294 }
295 if err := copyIfTable(tx, "user_keys", `INSERT INTO user_keys_new(fingerprint, user_name, type, line, comment)
296 SELECT k.fingerprint, u.name, k.type, k.line, k.comment FROM user_keys k JOIN users u ON u.id = k.user_id`); err != nil {
297 return err
298 }
299 if err := copyIfTable(tx, "instance_admins", `INSERT INTO instance_admins_new(user_name)
300 SELECT u.name FROM instance_admins ia JOIN users u ON u.id = ia.user_id`); err != nil {
301 return err
302 }
303 if err := copyIfTable(tx, "user_credentials", `INSERT INTO user_credentials_new(
304 credential_id, user_name, public_key, attestation_type, transports, flags, aaguid,
305 sign_count, clone_warning, attachment, label, created, last_used)
306 SELECT c.credential_id, u.name, c.public_key, c.attestation_type, c.transports, c.flags, c.aaguid,
307 c.sign_count, c.clone_warning, c.attachment, c.label, c.created, c.last_used
308 FROM user_credentials c JOIN users u ON u.id = c.user_id`); err != nil {
309 return err
310 }
311 if err := copyIfTable(tx, "user_invites", `INSERT INTO user_invites_new(token_hash, user_name, created, expires, used_at, created_by)
312 SELECT i.token_hash, u.name, i.created, i.expires, i.used_at, i.created_by
313 FROM user_invites i JOIN users u ON u.id = i.user_id`); err != nil {
314 return err
315 }
316 if err := copyIfTable(tx, "sessions", `INSERT INTO sessions_new(token_hash, user_name, credential_id, created, last_seen, expires, user_agent)
317 SELECT s.token_hash, u.name, s.credential_id, s.created, s.last_seen, s.expires, s.user_agent
318 FROM sessions s JOIN users u ON u.id = s.account_id`); err != nil {
319 return err
320 }
321
322 // Access rules and file locks live in the same database and name accounts by
323 // the old id. Rewrite them through the same mapping before the ids disappear.
324 if err := migrateUserReferences(tx); err != nil {
325 return err
326 }
327
328 // Drop the old tables children-first, then put the replacements in place.
329 for _, name := range []string{"sessions", "user_invites", "user_credentials",
330 "instance_admins", "user_keys", "user_names", "users"} {
331 if _, err := tx.Exec(`DROP TABLE IF EXISTS ` + name); err != nil {
332 return err
333 }
334 }
335 for _, rename := range [][2]string{
336 {"users_new", "users"},
337 {"user_credentials_new", "user_credentials"},
338 {"user_keys_new", "user_keys"},
339 {"instance_admins_new", "instance_admins"},
340 {"user_invites_new", "user_invites"},
341 {"sessions_new", "sessions"},
342 } {
343 if _, err := tx.Exec(`ALTER TABLE ` + rename[0] + ` RENAME TO ` + rename[1]); err != nil {
344 return err
345 }
346 }
347 if _, err := tx.Exec(`
348 CREATE INDEX user_keys_owner ON user_keys(user_name);
349 CREATE INDEX user_credentials_owner ON user_credentials(user_name);
350 CREATE INDEX user_invites_owner ON user_invites(user_name);
351 CREATE INDEX sessions_user ON sessions(user_name);
352 CREATE INDEX sessions_expiry ON sessions(expires);`); err != nil {
353 return err
354 }
355 return tx.Commit()
356 }
357
358 // migrateUserReferences rewrites the tables that store an account key as plain
359 // text rather than a foreign key. Only access rules and locks live that way; a
360 // missing table is normal on a database that never used those features.
361 func migrateUserReferences(tx *sql.Tx) error {
362 tableExists := func(name string) (bool, error) {
363 var n int
364 err := tx.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?`, name).Scan(&n)
365 return n > 0, err
366 }
367 hasACL, err := tableExists("repo_acl")
368 if err != nil {
254 369 return err
255 370 }
256 // Names used to be mutable, so every name an account had ever held was kept
257 // in a separate table to stop it being reissued. A name is now fixed at
258 // creation and the users row outlives retirement, so that table is redundant
259 // and its contents would otherwise be a list of aliases nothing resolves.
260 _, err := db.Exec(`DROP TABLE IF EXISTS user_names`)
371 hasLocks, err := tableExists("lfs_locks")
372 if err != nil {
373 return err
374 }
375 if !hasACL && !hasLocks {
376 return nil
377 }
378 if _, err := tx.Exec(`DROP TABLE IF EXISTS _user_id_map;
379 CREATE TABLE _user_id_map(old_id TEXT PRIMARY KEY, name TEXT NOT NULL);
380 INSERT INTO _user_id_map(old_id, name) SELECT id, name FROM users`); err != nil {
381 return err
382 }
383 if hasACL {
384 type aclRow struct {
385 repoID string
386 raw string
387 }
388 rows, err := tx.Query(`SELECT repo_id, acl FROM repo_acl`)
389 if err != nil {
390 return err
391 }
392 var updates []aclRow
393 for rows.Next() {
394 var r aclRow
395 if err := rows.Scan(&r.repoID, &r.raw); err != nil {
396 rows.Close()
397 return err
398 }
399 updates = append(updates, r)
400 }
401 rows.Close()
402 if err := rows.Err(); err != nil {
403 return err
404 }
405 for _, u := range updates {
406 rewritten, err := rewriteACLNames(tx, u.raw)
407 if err != nil {
408 return err
409 }
410 if rewritten == u.raw {
411 continue
412 }
413 if _, err := tx.Exec(`UPDATE repo_acl SET acl=? WHERE repo_id=?`, rewritten, u.repoID); err != nil {
414 return err
415 }
416 }
417 if _, err := tx.Exec(`UPDATE repo_acl SET updated_by=COALESCE(
418 (SELECT name FROM _user_id_map WHERE old_id=repo_acl.updated_by), updated_by)`); err != nil {
419 return err
420 }
421 }
422 if hasLocks {
423 if _, err := tx.Exec(`UPDATE lfs_locks SET owner=COALESCE(
424 (SELECT name FROM _user_id_map WHERE old_id=lfs_locks.owner), owner)
425 WHERE owner IN (SELECT old_id FROM _user_id_map)`); err != nil {
426 return err
427 }
428 }
429 _, err = tx.Exec(`DROP TABLE _user_id_map`)
430 return err
431 }
432
433 // rewriteACLNames replaces account ids with names inside an access-rule document.
434 // The fields are handled generically so a rule set written by a newer build keeps
435 // every setting this one does not know about.
436 func rewriteACLNames(tx *sql.Tx, raw string) (string, error) {
437 var doc map[string]any
438 if err := json.Unmarshal([]byte(raw), &doc); err != nil {
439 return "", fmt.Errorf("identity: read access rules during migration: %w", err)
440 }
441 lookup := func(id string) string {
442 var name string
443 // The mapping is in the transaction's table; a small query per key is
444 // fine because an ACL has a handful of entries.
445 if err := tx.QueryRow(`SELECT name FROM _user_id_map WHERE old_id=?`, id).Scan(&name); err == nil && name != "" {
446 return name
447 }
448 return id
449 }
450 if entries, ok := doc["entries"].(map[string]any); ok {
451 next := make(map[string]any, len(entries))
452 for id, role := range entries {
453 next[lookup(id)] = role
454 }
455 doc["entries"] = next
456 }
457 if runners, ok := doc["runners"].([]any); ok {
458 for i, r := range runners {
459 if id, ok := r.(string); ok {
460 runners[i] = lookup(id)
461 }
462 }
463 doc["runners"] = runners
464 }
465 out, err := json.MarshalIndent(doc, "", " ")
466 if err != nil {
467 return "", err
468 }
469 return string(out) + "\n", nil
470 }
471
472 func tableExists(db *sql.DB, table string) (bool, error) {
473 var n int
474 err := db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&n)
475 return n > 0, err
476 }
477
478 func columnExists(db *sql.DB, table, column string) (bool, error) {
479 rows, err := db.Query(`PRAGMA table_info(` + table + `)`)
480 if err != nil {
481 return false, err
482 }
483 defer rows.Close()
484 for rows.Next() {
485 var (
486 cid int
487 name string
488 typ string
489 notnull int
490 dflt any
491 pk int
492 )
493 if err := rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk); err != nil {
494 return false, err
495 }
496 if name == column {
497 return true, nil
498 }
499 }
500 return false, rows.Err()
501 }
502
503 func copyIfTable(tx *sql.Tx, table, statement string) error {
504 var n int
505 if err := tx.QueryRow(`SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?`, table).Scan(&n); err != nil {
506 return err
507 }
508 if n == 0 {
509 return nil
510 }
511 _, err := tx.Exec(statement)
261 512 return err
262 513 }
263 514
@@ -273,12 +524,13 @@ func Open(path string) (*Store, error) {
273 524 return nil, fmt.Errorf("identity: %w", err)
274 525 }
275 526 // Applying the schema here keeps an existing roster usable when a new table
276 // is added; every statement is CREATE IF NOT EXISTS.
277 if _, err := db.Exec(schema); err != nil {
527 // is added; every statement is CREATE IF NOT EXISTS. Migration runs first so
528 // the new indexes are only applied to tables that already have the new shape.
529 if err := migrate(db); err != nil {
278 530 db.Close()
279 531 return nil, fmt.Errorf("identity: %w", err)
280 532 }
281 if err := migrate(db); err != nil {
533 if _, err := db.Exec(schema); err != nil {
282 534 db.Close()
283 535 return nil, fmt.Errorf("identity: %w", err)
284 536 }
@@ -321,12 +573,11 @@ func (s *Store) Reload() error {
321 573
322 574 func (s *Store) load() (*Snapshot, error) {
323 575 snap := &Snapshot{
324 ByID: map[string]*Account{},
325 576 ByKey: map[string]string{},
326 577 ByName: map[string]*Account{},
327 578 Admins: map[string]bool{},
328 579 }
329 rows, err := s.db.Query(`SELECT id, name, display, status, created FROM users ORDER BY id`)
580 rows, err := s.db.Query(`SELECT name, status, created FROM users ORDER BY name`)
330 581 if err != nil {
331 582 return nil, fmt.Errorf("identity: read roster: %w", err)
332 583 }
@@ -335,34 +586,28 @@ func (s *Store) load() (*Snapshot, error) {
335 586 var a Account
336 587 var status string
337 588 var created int64
338 if err := rows.Scan(&a.ID, &a.Name, &a.Display, &status, &created); err != nil {
589 if err := rows.Scan(&a.Name, &status, &created); err != nil {
339 590 return nil, fmt.Errorf("identity: read roster: %w", err)
340 591 }
341 if !id.Valid(a.ID) {
342 return nil, fmt.Errorf("identity: %q is not a valid account id", a.ID)
343 }
344 592 if err := ValidateName(a.Name); err != nil {
345 return nil, fmt.Errorf("identity: account %s: %w", a.ID, err)
346 }
347 if err := ValidateDisplay(a.Display); err != nil {
348 return nil, fmt.Errorf("identity: account %s: %w", a.ID, err)
593 return nil, fmt.Errorf("identity: account %s: %w", a.Name, err)
349 594 }
350 595 switch status {
351 596 case "", "active":
352 597 status = "active"
353 598 case "retired":
354 599 default:
355 return nil, fmt.Errorf("identity: account %s: status must be active or retired", a.ID)
600 return nil, fmt.Errorf("identity: account %s: status must be active or retired", a.Name)
356 601 }
357 602 a.Status = status
358 603 a.Created = time.Unix(created, 0).UTC()
359 snap.ByID[a.ID] = &a
604 snap.ByName[a.Name] = &a
360 605 }
361 606 if err := rows.Err(); err != nil {
362 607 return nil, err
363 608 }
364 609
365 keys, err := s.db.Query(`SELECT fingerprint, user_id, type, line, comment FROM user_keys ORDER BY fingerprint`)
610 keys, err := s.db.Query(`SELECT fingerprint, user_name, type, line, comment FROM user_keys ORDER BY fingerprint`)
366 611 if err != nil {
367 612 return nil, fmt.Errorf("identity: read roster: %w", err)
368 613 }
@@ -373,7 +618,7 @@ func (s *Store) load() (*Snapshot, error) {
373 618 if err := keys.Scan(&k.Fingerprint, &owner, &k.Type, &k.Line, &k.Comment); err != nil {
374 619 return nil, fmt.Errorf("identity: read roster: %w", err)
375 620 }
376 a, ok := snap.ByID[owner]
621 a, ok := snap.ByName[owner]
377 622 if !ok {
378 623 return nil, fmt.Errorf("identity: key %s belongs to unknown account %s", k.Fingerprint, owner)
379 624 }
@@ -389,34 +634,28 @@ func (s *Store) load() (*Snapshot, error) {
389 634 return nil, err
390 635 }
391 636
392 admins, err := s.db.Query(`SELECT user_id FROM instance_admins`)
637 admins, err := s.db.Query(`SELECT user_name FROM instance_admins`)
393 638 if err != nil {
394 639 return nil, fmt.Errorf("identity: read instance admins: %w", err)
395 640 }
396 641 defer admins.Close()
397 642 for admins.Next() {
398 var accountID string
399 if err := admins.Scan(&accountID); err != nil {
643 var name string
644 if err := admins.Scan(&name); err != nil {
400 645 return nil, fmt.Errorf("identity: read instance admins: %w", err)
401 646 }
402 if _, ok := snap.ByID[accountID]; !ok {
403 return nil, fmt.Errorf("identity: instance admin %s is not an account", accountID)
647 if _, ok := snap.ByName[name]; !ok {
648 return nil, fmt.Errorf("identity: instance admin %s is not an account", name)
404 649 }
405 snap.Admins[accountID] = true
650 snap.Admins[name] = true
406 651 }
407 652 if err := admins.Err(); err != nil {
408 653 return nil, err
409 654 }
410 655
411 for _, a := range snap.ByID {
412 a.Admin = snap.Admins[a.ID]
656 for _, a := range snap.ByName {
657 a.Admin = snap.Admins[a.Name]
413 658 sort.Slice(a.Keys, func(i, j int) bool { return a.Keys[i].Fingerprint < a.Keys[j].Fingerprint })
414 // Every account, retired or not, keeps its name: the users row is the
415 // reservation, so a name can never be issued twice.
416 if other, ok := snap.ByName[a.Name]; ok {
417 return nil, fmt.Errorf("identity: two accounts are called %q (%s and %s)", a.Name, other.ID, a.ID)
418 }
419 snap.ByName[a.Name] = a
420 659 if a.Active() {
421 660 snap.Count++
422 661 }
@@ -425,115 +664,75 @@ func (s *Store) load() (*Snapshot, error) {
425 664 }
426 665
427 666 // IsAdmin reports whether an active account may use the remote admin API.
428 func (s *Store) IsAdmin(accountID string) bool {
667 func (s *Store) IsAdmin(name string) bool {
429 668 snap := s.Snapshot()
430 669 if snap == nil {
431 670 return false
432 671 }
433 a, ok := snap.ByID[accountID]
434 return ok && a.Active() && snap.Admins[accountID]
672 a, ok := snap.ByName[name]
673 return ok && a.Active() && snap.Admins[name]
435 674 }
436 675
437 676 // SetAdmin grants or revokes instance-admin access. It refuses to remove the
438 677 // last active instance admin, so a remote mistake cannot lock everybody out of
439 678 // the remote API.
440 func (s *Store) SetAdmin(idOrName string, admin bool, actor string) (*Account, error) {
679 func (s *Store) SetAdmin(name string, admin bool, actor string) (*Account, error) {
441 680 action := "identity.revoke-admin"
442 681 if admin {
443 682 action = "identity.grant-admin"
444 683 }
445 var accountID string
446 err := s.mutate(action, actor, idOrName, "", func(tx *sql.Tx) error {
447 a, err := findTx(tx, idOrName)
684 err := s.mutate(action, actor, name, "", func(tx *sql.Tx) error {
685 a, err := findTx(tx, name)
448 686 if err != nil {
449 687 return err
450 688 }
451 689 if !a.Active() {
452 690 return fmt.Errorf("identity: %s is retired", a.Name)
453 691 }
454 accountID = a.ID
455 692 if admin {
456 _, err := tx.Exec(`INSERT OR IGNORE INTO instance_admins(user_id) VALUES(?)`, a.ID)
693 _, err := tx.Exec(`INSERT OR IGNORE INTO instance_admins(user_name) VALUES(?)`, a.Name)
457 694 return err
458 695 }
459 696 var existing int
460 if err := tx.QueryRow(`SELECT 1 FROM instance_admins WHERE user_id=?`, a.ID).Scan(&existing); errors.Is(err, sql.ErrNoRows) {
697 if err := tx.QueryRow(`SELECT 1 FROM instance_admins WHERE user_name=?`, a.Name).Scan(&existing); errors.Is(err, sql.ErrNoRows) {
461 698 return fmt.Errorf("identity: %s is not an instance admin", a.Name)
462 699 } else if err != nil {
463 700 return err
464 701 }
465 702 var activeAdmins int
466 if err := tx.QueryRow(`SELECT COUNT(*) FROM instance_admins ia JOIN users u ON u.id=ia.user_id WHERE u.status='active'`).Scan(&activeAdmins); err != nil {
703 if err := tx.QueryRow(`SELECT COUNT(*) FROM instance_admins ia JOIN users u ON u.name=ia.user_name WHERE u.status='active'`).Scan(&activeAdmins); err != nil {
467 704 return err
468 705 }
469 706 if activeAdmins <= 1 {
470 707 return fmt.Errorf("identity: %s is the last instance admin; grant another admin before revoking this one", a.Name)
471 708 }
472 _, err = tx.Exec(`DELETE FROM instance_admins WHERE user_id=?`, a.ID)
709 _, err = tx.Exec(`DELETE FROM instance_admins WHERE user_name=?`, a.Name)
473 710 return err
474 711 })
475 712 if err != nil {
476 713 return nil, err
477 714 }
478 return s.Find(accountID)
715 return s.Find(name)
479 716 }
480 717
481 // Lookup resolves a key fingerprint to an active account id and name.
482 func (s *Store) Lookup(fingerprint string) (accountID, name string, ok bool) {
483 a, ok := s.Snapshot().ByID[s.Snapshot().ByKey[fingerprint]]
484 if !ok {
485 return "", "", false
486 }
487 return a.ID, a.Name, true
718 // Lookup resolves a key fingerprint to an active account name.
719 func (s *Store) Lookup(fingerprint string) (name string, ok bool) {
720 return s.Snapshot().Lookup(fingerprint)
488 721 }
489 722
490 // ResolveID maps an active name to its account id. A retired name resolves to
491 // nothing: the account is still known, but it is not a principal any more.
492 func (s *Store) ResolveID(name string) (string, bool) {
493 a, ok := s.Snapshot().ByName[name]
494 if !ok || !a.Active() {
495 return "", false
496 }
497 return a.ID, true
498 }
499
500 // Name returns the immutable name for an account id, or the id itself when the
501 // account is unknown. This is the identifier that may be written into a ref or a
502 // metadata file.
503 func (s *Store) Name(accountID string) string {
504 if a, ok := s.Snapshot().ByID[accountID]; ok {
505 return a.Name
506 }
507 return accountID
508 }
509
510 // Label returns the human-friendly form for an account id: the display name when
511 // one is set, the immutable name otherwise, and the id when the account is
512 // unknown. Nothing is authorized by the result, so it is only used for output.
513 func (s *Store) Label(accountID string) string {
514 if a, ok := s.Snapshot().ByID[accountID]; ok {
515 return a.Label()
516 }
517 return accountID
518 }
519
520 // Find resolves an id or a name, including the name of a retired account.
521 func (s *Store) Find(idOrName string) (*Account, error) {
522 snap := s.Snapshot()
523 if a, ok := snap.ByID[idOrName]; ok {
723 // Find resolves a name, including the name of a retired account.
724 func (s *Store) Find(name string) (*Account, error) {
725 if a, ok := s.Snapshot().ByName[name]; ok {
524 726 return a, nil
525 727 }
526 if a, ok := snap.ByName[idOrName]; ok {
527 return a, nil
528 }
529 return nil, fmt.Errorf("identity: no account %q", idOrName)
728 return nil, fmt.Errorf("identity: no account %q", name)
530 729 }
531 730
532 731 // All returns every account, active first, then by name.
533 732 func (s *Store) All() []*Account {
534 733 snap := s.Snapshot()
535 out := make([]*Account, 0, len(snap.ByID))
536 for _, a := range snap.ByID {
734 out := make([]*Account, 0, len(snap.ByName))
735 for _, a := range snap.ByName {
537 736 out = append(out, a)
538 737 }
539 738 sort.Slice(out, func(i, j int) bool {
@@ -545,14 +744,13 @@ func (s *Store) All() []*Account {
545 744 return out
546 745 }
547 746
548 // Roster returns the active accounts as id/name pairs, sorted by name. The SSH
549 // server serves this to authenticated clients so `vole change` can turn a name
550 // into the account id that ACLs and approvals store.
551 func (s *Store) Roster() []Account {
552 var out []Account
747 // Roster returns the active account names, sorted. The SSH server serves this to
748 // authenticated clients so `vole change` can check that a name is a real account.
749 func (s *Store) Roster() []string {
750 var out []string
553 751 for _, a := range s.All() {
554 752 if a.Active() {
555 out = append(out, Account{ID: a.ID, Name: a.Name})
753 out = append(out, a.Name)
556 754 }
557 755 }
558 756 return out
@@ -583,36 +781,28 @@ func (s *Store) mutate(action, actor, account, detail string, fn func(tx *sql.Tx
583 781 return nil
584 782 }
585 783
586 // Create adds an account with a fresh id, a fixed name, an optional display name
587 // and its first SSH key. The account, the name and the key are written in one
588 // transaction, so a name is never claimed by an account that cannot authenticate.
784 // Create adds an account with a fixed name and its first SSH key. The account,
785 // the name and the key are written in one transaction, so a name is never claimed
786 // by an account that cannot authenticate.
589 787 //
590 788 // An account that will use a passkey instead is created with CreateWithInvite;
591 789 // the invite is the credential-shaped way in.
592 func (s *Store) Create(name, display, authorizedKey, actor string) (*Account, error) {
790 func (s *Store) Create(name, authorizedKey, actor string) (*Account, error) {
593 791 if err := ValidateName(name); err != nil {
594 792 return nil, err
595 793 }
596 display = strings.TrimSpace(display)
597 if err := ValidateDisplay(display); err != nil {
598 return nil, err
599 }
600 794 key, err := parseKey(authorizedKey)
601 795 if err != nil {
602 796 return nil, err
603 797 }
604 798 now := time.Now().UTC()
605 accountID, err := id.New(now)
606 if err != nil {
607 return nil, err
608 }
609 err = s.mutate("identity.create", actor, accountID, name+" "+key.Fingerprint, func(tx *sql.Tx) error {
610 return createAccount(tx, name, display, accountID, now, &key, nil)
799 err = s.mutate("identity.create", actor, name, name+" "+key.Fingerprint, func(tx *sql.Tx) error {
800 return createAccount(tx, name, now, &key, nil)
611 801 })
612 802 if err != nil {
613 803 return nil, err
614 804 }
615 return s.Find(accountID)
805 return s.Find(name)
616 806 }
617 807
618 808 // pendingInvite is the row half of an enrollment link. The plaintext token is
@@ -623,7 +813,7 @@ type pendingInvite struct {
623 813 createdBy string
624 814 }
625 815
626 func createAccount(tx *sql.Tx, name, display, accountID string, now time.Time, key *Key, invite *pendingInvite) error {
816 func createAccount(tx *sql.Tx, name string, now time.Time, key *Key, invite *pendingInvite) error {
627 817 if owner, err := nameOwner(tx, name); err != nil {
628 818 return err
629 819 } else if owner != "" {
@@ -636,62 +826,35 @@ func createAccount(tx *sql.Tx, name, display, accountID string, now time.Time, k
636 826 return fmt.Errorf("identity: that key is already registered to %s", owner)
637 827 }
638 828 }
639 if _, err := tx.Exec(`INSERT INTO users(id, name, display, status, created) VALUES(?,?,?, 'active', ?)`,
640 accountID, name, display, now.Unix()); err != nil {
829 if _, err := tx.Exec(`INSERT INTO users(name, status, created) VALUES(?, 'active', ?)`,
830 name, now.Unix()); err != nil {
641 831 return fmt.Errorf("identity: create %s: %w", name, err)
642 832 }
643 833 if key != nil {
644 if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line, comment) VALUES(?,?,?,?,?)`,
645 key.Fingerprint, accountID, key.Type, key.Line, key.Comment); err != nil {
834 if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_name, type, line, comment) VALUES(?,?,?,?,?)`,
835 key.Fingerprint, name, key.Type, key.Line, key.Comment); err != nil {
646 836 return fmt.Errorf("identity: create %s: %w", name, err)
647 837 }
648 838 }
649 return insertInvite(tx, accountID, invite, now)
839 return insertInvite(tx, name, invite, now)
650 840 }
651 841
652 func insertInvite(tx *sql.Tx, accountID string, invite *pendingInvite, now time.Time) error {
842 func insertInvite(tx *sql.Tx, name string, invite *pendingInvite, now time.Time) error {
653 843 if invite == nil {
654 844 return nil
655 845 }
656 if _, err := tx.Exec(`INSERT INTO user_invites(token_hash, user_id, created, expires, created_by) VALUES(?,?,?,?,?)`,
657 invite.tokenHash, accountID, now.Unix(), invite.expires.Unix(), invite.createdBy); err != nil {
846 if _, err := tx.Exec(`INSERT INTO user_invites(token_hash, user_name, created, expires, created_by) VALUES(?,?,?,?,?)`,
847 invite.tokenHash, name, now.Unix(), invite.expires.Unix(), invite.createdBy); err != nil {
658 848 return fmt.Errorf("identity: create invite: %w", err)
659 849 }
660 850 return nil
661 851 }
662 852
663 // SetDisplay replaces the human-friendly label shown next to an account's fixed
664 // name. An empty display clears it and the name is shown instead. The name is
665 // not touched: it is what refs and metadata files were written with.
666 func (s *Store) SetDisplay(idOrName, display, actor string) (*Account, error) {
667 display = strings.TrimSpace(display)
668 if err := ValidateDisplay(display); err != nil {
669 return nil, err
670 }
671 var accountID string
672 err := s.mutate("identity.display", actor, idOrName, display, func(tx *sql.Tx) error {
673 a, err := findTx(tx, idOrName)
674 if err != nil {
675 return err
676 }
677 if _, err := tx.Exec(`UPDATE users SET display=? WHERE id=?`, display, a.ID); err != nil {
678 return fmt.Errorf("identity: display %s: %w", a.ID, err)
679 }
680 accountID = a.ID
681 return nil
682 })
683 if err != nil {
684 return nil, err
685 }
686 return s.Find(accountID)
687 }
688
689 853 // Retire disables an account. Its keys stop authenticating at the next reload and
690 854 // its name is never reissued.
691 func (s *Store) Retire(idOrName, actor string) (*Account, error) {
692 var accountID string
693 err := s.mutate("identity.retire", actor, idOrName, "", func(tx *sql.Tx) error {
694 a, err := findTx(tx, idOrName)
855 func (s *Store) Retire(name, actor string) (*Account, error) {
856 err := s.mutate("identity.retire", actor, name, "", func(tx *sql.Tx) error {
857 a, err := findTx(tx, name)
695 858 if err != nil {
696 859 return err
697 860 }
@@ -699,9 +862,9 @@ func (s *Store) Retire(idOrName, actor string) (*Account, error) {
699 862 return fmt.Errorf("identity: %s is already retired", a.Name)
700 863 }
701 864 var isAdmin bool
702 if err := tx.QueryRow(`SELECT 1 FROM instance_admins WHERE user_id=?`, a.ID).Scan(&isAdmin); err == nil && isAdmin {
865 if err := tx.QueryRow(`SELECT 1 FROM instance_admins WHERE user_name=?`, a.Name).Scan(&isAdmin); err == nil && isAdmin {
703 866 var activeAdmins int
704 if err := tx.QueryRow(`SELECT COUNT(*) FROM instance_admins ia JOIN users u ON u.id=ia.user_id WHERE u.status='active'`).Scan(&activeAdmins); err != nil {
867 if err := tx.QueryRow(`SELECT COUNT(*) FROM instance_admins ia JOIN users u ON u.name=ia.user_name WHERE u.status='active'`).Scan(&activeAdmins); err != nil {
705 868 return err
706 869 }
707 870 if activeAdmins <= 1 {
@@ -710,25 +873,24 @@ func (s *Store) Retire(idOrName, actor string) (*Account, error) {
710 873 } else if err != nil && !errors.Is(err, sql.ErrNoRows) {
711 874 return err
712 875 }
713 if _, err := tx.Exec(`UPDATE users SET status='retired' WHERE id=?`, a.ID); err != nil {
714 return fmt.Errorf("identity: retire %s: %w", a.ID, err)
876 if _, err := tx.Exec(`UPDATE users SET status='retired' WHERE name=?`, a.Name); err != nil {
877 return fmt.Errorf("identity: retire %s: %w", a.Name, err)
715 878 }
716 879 // Retiring is the one act that must end a browser session immediately.
717 880 // The session lookup checks status too, but deleting here also clears the
718 881 // rows and any outstanding enrollment links.
719 if _, err := tx.Exec(`DELETE FROM sessions WHERE account_id=?`, a.ID); err != nil {
720 return fmt.Errorf("identity: retire %s: %w", a.ID, err)
882 if _, err := tx.Exec(`DELETE FROM sessions WHERE user_name=?`, a.Name); err != nil {
883 return fmt.Errorf("identity: retire %s: %w", a.Name, err)
721 884 }
722 if _, err := tx.Exec(`DELETE FROM user_invites WHERE user_id=?`, a.ID); err != nil {
723 return fmt.Errorf("identity: retire %s: %w", a.ID, err)
885 if _, err := tx.Exec(`DELETE FROM user_invites WHERE user_name=?`, a.Name); err != nil {
886 return fmt.Errorf("identity: retire %s: %w", a.Name, err)
724 887 }
725 accountID = a.ID
726 888 return nil
727 889 })
728 890 if err != nil {
729 891 return nil, err
730 892 }
731 return s.Find(accountID)
893 return s.Find(name)
732 894 }
733 895
734 896 // parseKey validates one authorized_keys line and extracts what the roster stores.
@@ -753,7 +915,7 @@ func parseKey(authorizedKey string) (Key, error) {
753 915 // the key is unclaimed.
754 916 func keyOwner(tx *sql.Tx, fingerprint string) (string, error) {
755 917 var owner string
756 err := tx.QueryRow(`SELECT u.name FROM user_keys k JOIN users u ON u.id=k.user_id WHERE k.fingerprint=?`, fingerprint).Scan(&owner)
918 err := tx.QueryRow(`SELECT user_name FROM user_keys WHERE fingerprint=?`, fingerprint).Scan(&owner)
757 919 if errors.Is(err, sql.ErrNoRows) {
758 920 return "", nil
759 921 }
@@ -765,13 +927,13 @@ func keyOwner(tx *sql.Tx, fingerprint string) (string, error) {
765 927
766 928 // AddKey attaches an authorized_keys line to an account and returns its
767 929 // fingerprint. Creation requires a key; this is for adding another one.
768 func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) {
930 func (s *Store) AddKey(name, authorizedKey, actor string) (string, error) {
769 931 key, err := parseKey(authorizedKey)
770 932 if err != nil {
771 933 return "", err
772 934 }
773 err = s.mutate("identity.add-key", actor, idOrName, key.Fingerprint+" "+key.Comment, func(tx *sql.Tx) error {
774 a, err := findTx(tx, idOrName)
935 err = s.mutate("identity.add-key", actor, name, key.Fingerprint+" "+key.Comment, func(tx *sql.Tx) error {
936 a, err := findTx(tx, name)
775 937 if err != nil {
776 938 return err
777 939 }
@@ -785,8 +947,8 @@ func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) {
785 947 case owner != "":
786 948 return fmt.Errorf("identity: that key is already registered to %s", owner)
787 949 }
788 if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line, comment) VALUES(?,?,?,?,?)`,
789 key.Fingerprint, a.ID, key.Type, key.Line, key.Comment); err != nil {
950 if _, err := tx.Exec(`INSERT INTO user_keys(fingerprint, user_name, type, line, comment) VALUES(?,?,?,?,?)`,
951 key.Fingerprint, a.Name, key.Type, key.Line, key.Comment); err != nil {
790 952 return fmt.Errorf("identity: add key: %w", err)
791 953 }
792 954 return nil
@@ -798,13 +960,13 @@ func (s *Store) AddKey(idOrName, authorizedKey, actor string) (string, error) {
798 960 }
799 961
800 962 // RemoveKey revokes one key. An exact fingerprint or a unique prefix is accepted.
801 func (s *Store) RemoveKey(idOrName, fingerprint, actor string) error {
802 return s.mutate("identity.remove-key", actor, idOrName, fingerprint, func(tx *sql.Tx) error {
803 a, err := findTx(tx, idOrName)
963 func (s *Store) RemoveKey(name, fingerprint, actor string) error {
964 return s.mutate("identity.remove-key", actor, name, fingerprint, func(tx *sql.Tx) error {
965 a, err := findTx(tx, name)
804 966 if err != nil {
805 967 return err
806 968 }
807 rows, err := tx.Query(`SELECT fingerprint FROM user_keys WHERE user_id=?`, a.ID)
969 rows, err := tx.Query(`SELECT fingerprint FROM user_keys WHERE user_name=?`, a.Name)
808 970 if err != nil {
809 971 return fmt.Errorf("identity: read keys: %w", err)
810 972 }
@@ -836,12 +998,12 @@ func (s *Store) RemoveKey(idOrName, fingerprint, actor string) error {
836 998 })
837 999 }
838 1000
839 // nameOwner returns the account id holding name, or "" when the name is unused.
1001 // nameOwner returns the account name when it is taken, or "" when it is unused.
840 1002 // Retired accounts count: their row is the reservation that stops a name being
841 1003 // reissued.
842 1004 func nameOwner(tx *sql.Tx, name string) (string, error) {
843 1005 var owner string
844 err := tx.QueryRow(`SELECT id FROM users WHERE name=?`, name).Scan(&owner)
1006 err := tx.QueryRow(`SELECT name FROM users WHERE name=?`, name).Scan(&owner)
845 1007 if errors.Is(err, sql.ErrNoRows) {
846 1008 return "", nil
847 1009 }
@@ -851,18 +1013,15 @@ func nameOwner(tx *sql.Tx, name string) (string, error) {
851 1013 return owner, nil
852 1014 }
853 1015
854 // findTx resolves an id (any status) or an active name inside a transaction.
855 func findTx(tx *sql.Tx, idOrName string) (*Account, error) {
1016 // findTx resolves a name inside a transaction, including a retired one. Callers
1017 // that require an active account check the status themselves.
1018 func findTx(tx *sql.Tx, name string) (*Account, error) {
856 1019 var a Account
857 1020 var created int64
858 err := tx.QueryRow(`SELECT id, name, status, created FROM users WHERE id=?`, idOrName).
859 Scan(&a.ID, &a.Name, &a.Status, &created)
860 if errors.Is(err, sql.ErrNoRows) {
861 err = tx.QueryRow(`SELECT id, name, status, created FROM users WHERE name=? AND status='active'`, idOrName).
862 Scan(&a.ID, &a.Name, &a.Status, &created)
863 }
1021 err := tx.QueryRow(`SELECT name, status, created FROM users WHERE name=?`, name).
1022 Scan(&a.Name, &a.Status, &created)
864 1023 if errors.Is(err, sql.ErrNoRows) {
865 return nil, fmt.Errorf("identity: no active account %q", idOrName)
1024 return nil, fmt.Errorf("identity: no account %q", name)
866 1025 }
867 1026 if err != nil {
868 1027 return nil, fmt.Errorf("identity: read roster: %w", err)