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_test.go +153−132
| @@ -9,7 +9,6 @@ import ( | |||
| 9 | 9 | "sync" | |
| 10 | 10 | "testing" | |
| 11 | 11 | ||
| 12 | "github.com/rodent-software/vole/core/id" | ||
| 13 | 12 | "golang.org/x/crypto/ssh" | |
| 14 | 13 | ) | |
| 15 | 14 | ||
| @@ -49,7 +48,7 @@ func mustCreate(t *testing.T, s *Store, name string) *Account { | |||
| 49 | 48 | func mustCreateBy(t *testing.T, s *Store, name, actor string) *Account { | |
| 50 | 49 | t.Helper() | |
| 51 | 50 | line, _ := pubLine(t) | |
| 52 | a, err := s.Create(name, "", line, actor) | ||
| 51 | a, err := s.Create(name, line, actor) | ||
| 53 | 52 | if err != nil { | |
| 54 | 53 | t.Fatalf("create %s: %v", name, err) | |
| 55 | 54 | } | |
| @@ -75,22 +74,15 @@ func TestCreateAccountWithKeyAndAddKey(t *testing.T) { | |||
| 75 | 74 | t.Parallel() | |
| 76 | 75 | s := newStore(t) | |
| 77 | 76 | line, wantFP := pubLine(t) | |
| 78 | a, err := s.Create("keenan", "", line, "test") | ||
| 77 | a, err := s.Create("keenan", line, "test") | ||
| 79 | 78 | if err != nil { | |
| 80 | 79 | t.Fatal(err) | |
| 81 | 80 | } | |
| 82 | if !id.Valid(a.ID) { | ||
| 83 | t.Fatalf("account id %q is not a ULID", a.ID) | ||
| 84 | } | ||
| 85 | 81 | if len(a.Keys) != 1 || a.Keys[0].Fingerprint != wantFP { | |
| 86 | 82 | t.Fatalf("created account keys = %+v, want the supplied key %s", a.Keys, wantFP) | |
| 87 | 83 | } | |
| 88 | accountID, name, ok := s.Lookup(wantFP) | ||
| 89 | if !ok || accountID != a.ID || name != "keenan" { | ||
| 90 | t.Fatalf("Lookup = %q,%q,%v; want %q,keenan,true", accountID, name, ok, a.ID) | ||
| 91 | } | ||
| 92 | if got, ok := s.ResolveID("keenan"); !ok || got != a.ID { | ||
| 93 | t.Fatalf("ResolveID = %q,%v", got, ok) | ||
| 84 | if name, ok := s.Lookup(wantFP); !ok || name != "keenan" { | ||
| 85 | t.Fatalf("Lookup = %q,%v; want keenan,true", name, ok) | ||
| 94 | 86 | } | |
| 95 | 87 | ||
| 96 | 88 | // add-key remains the way to attach a second credential. | |
| @@ -111,7 +103,7 @@ func TestCreateRequiresAKeyAndDoesNotReserveTheName(t *testing.T) { | |||
| 111 | 103 | t.Parallel() | |
| 112 | 104 | s := newStore(t) | |
| 113 | 105 | for _, bad := range []string{"", "not an SSH key"} { | |
| 114 | if _, err := s.Create("alice", "", bad, "test"); err == nil { | ||
| 106 | if _, err := s.Create("alice", bad, "test"); err == nil { | ||
| 115 | 107 | t.Fatalf("Create accepted key %q", bad) | |
| 116 | 108 | } | |
| 117 | 109 | } | |
| @@ -119,7 +111,7 @@ func TestCreateRequiresAKeyAndDoesNotReserveTheName(t *testing.T) { | |||
| 119 | 111 | t.Fatal("a failed create left an account behind") | |
| 120 | 112 | } | |
| 121 | 113 | line, _ := pubLine(t) | |
| 122 | a, err := s.Create("alice", "", line, "test") | ||
| 114 | a, err := s.Create("alice", line, "test") | ||
| 123 | 115 | if err != nil { | |
| 124 | 116 | t.Fatalf("the failed attempts reserved the name: %v", err) | |
| 125 | 117 | } | |
| @@ -134,10 +126,10 @@ func TestCreateRejectsAKeyAlreadyRegistered(t *testing.T) { | |||
| 134 | 126 | t.Parallel() | |
| 135 | 127 | s := newStore(t) | |
| 136 | 128 | line, _ := pubLine(t) | |
| 137 | if _, err := s.Create("alice", "", line, "test"); err != nil { | ||
| 129 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 138 | 130 | t.Fatal(err) | |
| 139 | 131 | } | |
| 140 | if _, err := s.Create("bob", "", line, "test"); err == nil { | ||
| 132 | if _, err := s.Create("bob", line, "test"); err == nil { | ||
| 141 | 133 | t.Fatal("a registered key was accepted for a second account") | |
| 142 | 134 | } else if !strings.Contains(err.Error(), "already registered") { | |
| 143 | 135 | t.Fatalf("unclear error: %v", err) | |
| @@ -146,7 +138,7 @@ func TestCreateRejectsAKeyAlreadyRegistered(t *testing.T) { | |||
| 146 | 138 | t.Fatal("the duplicate-key failure left an account behind") | |
| 147 | 139 | } | |
| 148 | 140 | fresh, _ := pubLine(t) | |
| 149 | if _, err := s.Create("bob", "", fresh, "test"); err != nil { | ||
| 141 | if _, err := s.Create("bob", fresh, "test"); err != nil { | ||
| 150 | 142 | t.Fatalf("the duplicate-key failure reserved bob's name: %v", err) | |
| 151 | 143 | } | |
| 152 | 144 | } | |
| @@ -155,7 +147,7 @@ func TestMultipleKeysPerAccount(t *testing.T) { | |||
| 155 | 147 | t.Parallel() | |
| 156 | 148 | s := newStore(t) | |
| 157 | 149 | line, _ := pubLine(t) | |
| 158 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 150 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 159 | 151 | t.Fatal(err) | |
| 160 | 152 | } | |
| 161 | 153 | var fps []string | |
| @@ -178,7 +170,7 @@ func TestMultipleKeysPerAccount(t *testing.T) { | |||
| 178 | 170 | t.Fatalf("keys = %d, want 3", got) | |
| 179 | 171 | } | |
| 180 | 172 | for _, fp := range fps { | |
| 181 | if _, _, ok := s.Lookup(fp); !ok { | ||
| 173 | if _, ok := s.Lookup(fp); !ok { | ||
| 182 | 174 | t.Fatalf("key %s lost", fp) | |
| 183 | 175 | } | |
| 184 | 176 | } | |
| @@ -188,11 +180,11 @@ func TestDuplicateKeyAcrossAccountsRejected(t *testing.T) { | |||
| 188 | 180 | t.Parallel() | |
| 189 | 181 | s := newStore(t) | |
| 190 | 182 | line, _ := pubLine(t) | |
| 191 | if _, err := s.Create("alice", "", line, "test"); err != nil { | ||
| 183 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 192 | 184 | t.Fatal(err) | |
| 193 | 185 | } | |
| 194 | 186 | bobLine, _ := pubLine(t) | |
| 195 | if _, err := s.Create("bob", "", bobLine, "test"); err != nil { | ||
| 187 | if _, err := s.Create("bob", bobLine, "test"); err != nil { | ||
| 196 | 188 | t.Fatal(err) | |
| 197 | 189 | } | |
| 198 | 190 | if _, err := s.AddKey("bob", line, "test"); err == nil { | |
| @@ -224,7 +216,7 @@ func TestOneKeyBelongsToOneAccount(t *testing.T) { | |||
| 224 | 216 | } | |
| 225 | 217 | defer raw.Close() | |
| 226 | 218 | if _, err := raw.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line) VALUES(?,?,'ssh-ed25519',?)`, | |
| 227 | fp, bob.ID, line); err == nil { | ||
| 219 | fp, bob.Name, line); err == nil { | ||
| 228 | 220 | t.Fatal("a second account was allowed to claim a registered key") | |
| 229 | 221 | } | |
| 230 | 222 | } | |
| @@ -240,7 +232,7 @@ func TestCorruptRosterFailsStartup(t *testing.T) { | |||
| 240 | 232 | if err != nil { | |
| 241 | 233 | t.Fatal(err) | |
| 242 | 234 | } | |
| 243 | if _, err := raw.Exec(`UPDATE users SET name='Not A Name'`); err != nil { | ||
| 235 | if _, err := raw.Exec(`INSERT INTO users(name, status, created) VALUES('Not A Name', 'active', 0)`); err != nil { | ||
| 244 | 236 | t.Fatal(err) | |
| 245 | 237 | } | |
| 246 | 238 | raw.Close() | |
| @@ -260,7 +252,7 @@ func TestRemoveKey(t *testing.T) { | |||
| 260 | 252 | s := newStore(t) | |
| 261 | 253 | keepLine, keepFP := pubLine(t) | |
| 262 | 254 | dropLine, dropFP := pubLine(t) | |
| 263 | if _, err := s.Create("keenan", "", keepLine, "test"); err != nil { | ||
| 255 | if _, err := s.Create("keenan", keepLine, "test"); err != nil { | ||
| 264 | 256 | t.Fatal(err) | |
| 265 | 257 | } | |
| 266 | 258 | if _, err := s.AddKey("keenan", dropLine, "test"); err != nil { | |
| @@ -269,10 +261,10 @@ func TestRemoveKey(t *testing.T) { | |||
| 269 | 261 | if err := s.RemoveKey("keenan", dropFP, "test"); err != nil { | |
| 270 | 262 | t.Fatal(err) | |
| 271 | 263 | } | |
| 272 | if _, _, ok := s.Lookup(dropFP); ok { | ||
| 264 | if _, ok := s.Lookup(dropFP); ok { | ||
| 273 | 265 | t.Fatal("revoked key still authenticates") | |
| 274 | 266 | } | |
| 275 | if _, _, ok := s.Lookup(keepFP); !ok { | ||
| 267 | if _, ok := s.Lookup(keepFP); !ok { | ||
| 276 | 268 | t.Fatal("removing one key took the other with it") | |
| 277 | 269 | } | |
| 278 | 270 | } | |
| @@ -283,107 +275,50 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 283 | 275 | t.Parallel() | |
| 284 | 276 | s := newStore(t) | |
| 285 | 277 | line, fp := pubLine(t) | |
| 286 | a, err := s.Create("wilbur", "", line, "test") | ||
| 278 | a, err := s.Create("wilbur", line, "test") | ||
| 287 | 279 | if err != nil { | |
| 288 | 280 | t.Fatal(err) | |
| 289 | 281 | } | |
| 290 | 282 | if _, err := s.Retire("wilbur", "test"); err != nil { | |
| 291 | 283 | t.Fatal(err) | |
| 292 | 284 | } | |
| 293 | if _, _, ok := s.Lookup(fp); ok { | ||
| 285 | if _, ok := s.Lookup(fp); ok { | ||
| 294 | 286 | t.Fatal("a retired account's key still authenticates") | |
| 295 | 287 | } | |
| 296 | if _, ok := s.ResolveID("wilbur"); ok { | ||
| 297 | t.Fatal("a retired name still resolves") | ||
| 298 | } | ||
| 299 | 288 | reissue, _ := pubLine(t) | |
| 300 | if _, err := s.Create("wilbur", "", reissue, "test"); err == nil { | ||
| 289 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 301 | 290 | t.Fatalf("the retired name was reissued to a new account") | |
| 302 | 291 | } else if !strings.Contains(err.Error(), "cannot be reused") { | |
| 303 | 292 | t.Fatalf("unclear error: %v", err) | |
| 304 | 293 | } | |
| 305 | // The account itself still exists for display and history. | ||
| 306 | if got, err := s.Find(a.ID); err != nil || got.Status != "retired" { | ||
| 294 | // The account itself still exists for history. | ||
| 295 | if got, err := s.Find(a.Name); err != nil || got.Status != "retired" { | ||
| 307 | 296 | t.Fatalf("Find after retire = %v, %v", got, err) | |
| 308 | 297 | } | |
| 309 | 298 | } | |
| 310 | 299 | ||
| 311 | // TestDisplayNameIsALabelAndTheNameIsFixed: the display name is what people | ||
| 312 | // read, and it may change; the account name is what refs and metadata files were | ||
| 313 | // written with, so it may not. | ||
| 314 | func TestDisplayNameIsALabelAndTheNameIsFixed(t *testing.T) { | ||
| 300 | // TestNameIsTheOnlyIdentity: an account is its registered name. The name may | ||
| 301 | // not be taken by a second account, retired or not. | ||
| 302 | func TestNameIsTheOnlyIdentity(t *testing.T) { | ||
| 315 | 303 | t.Parallel() | |
| 316 | 304 | s := newStore(t) | |
| 317 | 305 | line, fp := pubLine(t) | |
| 318 | a, err := s.Create("wilbur", "Wilbur Wright", line, "test") | ||
| 306 | a, err := s.Create("wilbur", line, "test") | ||
| 319 | 307 | if err != nil { | |
| 320 | 308 | t.Fatal(err) | |
| 321 | 309 | } | |
| 322 | if a.Label() != "Wilbur Wright" { | ||
| 323 | t.Fatalf("Label = %q, want the display name", a.Label()) | ||
| 324 | } | ||
| 325 | // The display name is not an identity: only the fixed name resolves. | ||
| 326 | if _, ok := s.ResolveID("Wilbur Wright"); ok { | ||
| 327 | t.Fatal("a display name resolved as an account name") | ||
| 310 | if a.Name != "wilbur" { | ||
| 311 | t.Fatalf("created account name = %q", a.Name) | ||
| 328 | 312 | } | |
| 329 | if got, ok := s.ResolveID("wilbur"); !ok || got != a.ID { | ||
| 330 | t.Fatalf("ResolveID(wilbur) = %q,%v; want %s", got, ok, a.ID) | ||
| 313 | if name, ok := s.Lookup(fp); !ok || name != a.Name { | ||
| 314 | t.Fatalf("Lookup = %q,%v; want %q", name, ok, a.Name) | ||
| 331 | 315 | } | |
| 332 | |||
| 333 | changed, err := s.SetDisplay("wilbur", "Wilbur", "test") | ||
| 334 | if err != nil { | ||
| 335 | t.Fatal(err) | ||
| 336 | } | ||
| 337 | if changed.ID != a.ID || changed.Name != "wilbur" { | ||
| 338 | t.Fatalf("SetDisplay touched the name: %+v", changed) | ||
| 339 | } | ||
| 340 | if changed.Label() != "Wilbur" { | ||
| 341 | t.Fatalf("Label after SetDisplay = %q", changed.Label()) | ||
| 342 | } | ||
| 343 | if got, _, ok := s.Lookup(fp); !ok || got != a.ID { | ||
| 344 | t.Fatalf("Lookup after SetDisplay = %q,%v; want %s", got, ok, a.ID) | ||
| 345 | } | ||
| 346 | |||
| 347 | // Clearing the display falls back to the name. | ||
| 348 | cleared, err := s.SetDisplay(a.ID, "", "test") | ||
| 349 | if err != nil { | ||
| 350 | t.Fatal(err) | ||
| 351 | } | ||
| 352 | if cleared.Display != "" || cleared.Label() != "wilbur" { | ||
| 353 | t.Fatalf("cleared display = %q, label %q", cleared.Display, cleared.Label()) | ||
| 354 | } | ||
| 355 | |||
| 356 | // The name cannot be taken by a second account, and the API that used to | ||
| 357 | // move it is gone. | ||
| 358 | 316 | reissue, _ := pubLine(t) | |
| 359 | if _, err := s.Create("wilbur", "", reissue, "test"); err == nil { | ||
| 317 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 360 | 318 | t.Fatal("a second account took the name") | |
| 361 | 319 | } | |
| 362 | if _, err := s.SetDisplay("nobody", "x", "test"); err == nil { | ||
| 363 | t.Fatal("SetDisplay accepted an unknown account") | ||
| 364 | } | ||
| 365 | } | ||
| 366 | |||
| 367 | func TestInvalidDisplaysRefused(t *testing.T) { | ||
| 368 | t.Parallel() | ||
| 369 | s := newStore(t) | ||
| 370 | line, _ := pubLine(t) | ||
| 371 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 372 | t.Fatal(err) | ||
| 373 | } | ||
| 374 | for _, display := range []string{"two\nlines", "tab\there", strings.Repeat("x", maxDisplayRunes+1)} { | ||
| 375 | if err := ValidateDisplay(display); err == nil { | ||
| 376 | t.Fatalf("ValidateDisplay accepted %q", display) | ||
| 377 | } | ||
| 378 | if _, err := s.SetDisplay("keenan", display, "test"); err == nil { | ||
| 379 | t.Fatalf("SetDisplay accepted %q", display) | ||
| 380 | } | ||
| 381 | } | ||
| 382 | // Spaces, capitals and non-Latin scripts are exactly what a label is for. | ||
| 383 | for _, display := range []string{"Keenan O'Brien", "田中 健", " padded "} { | ||
| 384 | if _, err := s.SetDisplay("keenan", display, "test"); err != nil { | ||
| 385 | t.Fatalf("SetDisplay(%q): %v", display, err) | ||
| 386 | } | ||
| 320 | if _, err := s.Find("nobody"); err == nil { | ||
| 321 | t.Fatal("Find accepted an unknown account") | ||
| 387 | 322 | } | |
| 388 | 323 | } | |
| 389 | 324 | ||
| @@ -392,7 +327,7 @@ func TestInvalidNamesRefused(t *testing.T) { | |||
| 392 | 327 | s := newStore(t) | |
| 393 | 328 | line, _ := pubLine(t) | |
| 394 | 329 | for _, name := range []string{"Keenan", "has space", "a", "with/slash", "trailing."} { | |
| 395 | if _, err := s.Create(name, "", line, "test"); err == nil { | ||
| 330 | if _, err := s.Create(name, line, "test"); err == nil { | ||
| 396 | 331 | t.Fatalf("%q accepted as a name", name) | |
| 397 | 332 | } | |
| 398 | 333 | } | |
| @@ -408,7 +343,7 @@ func TestConcurrentAddsAllLand(t *testing.T) { | |||
| 408 | 343 | for i := range lines { | |
| 409 | 344 | lines[i], _ = pubLine(t) | |
| 410 | 345 | } | |
| 411 | if _, err := s.Create("keenan", "", lines[0], "test"); err != nil { | ||
| 346 | if _, err := s.Create("keenan", lines[0], "test"); err != nil { | ||
| 412 | 347 | t.Fatal(err) | |
| 413 | 348 | } | |
| 414 | 349 | var wg sync.WaitGroup | |
| @@ -438,16 +373,13 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 438 | 373 | t.Parallel() | |
| 439 | 374 | s := newStore(t) | |
| 440 | 375 | line, fp := pubLine(t) | |
| 441 | if _, err := s.Create("keenan", "", line, "test"); err != nil { | ||
| 376 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 442 | 377 | t.Fatal(err) | |
| 443 | 378 | } | |
| 444 | 379 | second, secondFP := pubLine(t) | |
| 445 | 380 | if _, err := s.AddKey("keenan", second, "test"); err != nil { | |
| 446 | 381 | t.Fatal(err) | |
| 447 | 382 | } | |
| 448 | if _, err := s.SetDisplay("keenan", "Keenan", "test"); err != nil { | ||
| 449 | t.Fatal(err) | ||
| 450 | } | ||
| 451 | 383 | if err := s.RemoveKey("keenan", secondFP, "test"); err != nil { | |
| 452 | 384 | t.Fatal(err) | |
| 453 | 385 | } | |
| @@ -456,28 +388,29 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 456 | 388 | t.Fatal(err) | |
| 457 | 389 | } | |
| 458 | 390 | log := string(data) | |
| 459 | for _, want := range []string{"identity.create", "identity.add-key", "identity.display", "identity.remove-key", fp, secondFP} { | ||
| 391 | for _, want := range []string{"identity.create", "identity.add-key", "identity.remove-key", fp, secondFP} { | ||
| 460 | 392 | if !strings.Contains(log, want) { | |
| 461 | 393 | t.Fatalf("audit log missing %q:\n%s", want, log) | |
| 462 | 394 | } | |
| 463 | 395 | } | |
| 464 | 396 | } | |
| 465 | 397 | ||
| 466 | // TestRosterUpgradesFromTheAliasTable: rosters written before names were fixed | ||
| 467 | // kept a user_names alias table and no display column. Opening one must add the | ||
| 468 | // column, keep every account readable, and drop the alias table rather than | ||
| 469 | // leave a list of former names nothing resolves. | ||
| 470 | func TestRosterUpgradesFromTheAliasTable(t *testing.T) { | ||
| 398 | // TestRosterUpgradesFromTheOldSchema: rosters used to key accounts by an internal | ||
| 399 | // id. Opening one must rebuild every table around the registered name and drop | ||
| 400 | // the alias table rather than leave a list of former names nothing resolves. | ||
| 401 | func TestRosterUpgradesFromTheOldSchema(t *testing.T) { | ||
| 471 | 402 | t.Parallel() | |
| 472 | 403 | path := filepath.Join(t.TempDir(), "users.db") | |
| 473 | 404 | db, err := openDB(path) | |
| 474 | 405 | if err != nil { | |
| 475 | 406 | t.Fatal(err) | |
| 476 | 407 | } | |
| 408 | keyLine, keyFP := pubLine(t) | ||
| 477 | 409 | if _, err := db.Exec(` | |
| 478 | 410 | CREATE TABLE users ( | |
| 479 | 411 | id TEXT PRIMARY KEY, | |
| 480 | 412 | name TEXT NOT NULL UNIQUE, | |
| 413 | display TEXT NOT NULL DEFAULT '', | ||
| 481 | 414 | status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','retired')), | |
| 482 | 415 | created INTEGER NOT NULL | |
| 483 | 416 | ); | |
| @@ -485,9 +418,77 @@ CREATE TABLE user_names ( | |||
| 485 | 418 | name TEXT PRIMARY KEY, | |
| 486 | 419 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE | |
| 487 | 420 | ); | |
| 488 | INSERT INTO users(id, name, status, created) VALUES('01M5JR2JK29KKMAA1T9MDNTPBP', 'wilbur', 'active', 0); | ||
| 421 | CREATE TABLE user_keys ( | ||
| 422 | fingerprint TEXT PRIMARY KEY, | ||
| 423 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 424 | type TEXT NOT NULL, | ||
| 425 | line TEXT NOT NULL, | ||
| 426 | comment TEXT NOT NULL DEFAULT '' | ||
| 427 | ); | ||
| 428 | CREATE TABLE instance_admins ( | ||
| 429 | user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE | ||
| 430 | ); | ||
| 431 | CREATE TABLE user_credentials ( | ||
| 432 | credential_id TEXT PRIMARY KEY, | ||
| 433 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 434 | public_key BLOB NOT NULL, | ||
| 435 | attestation_type TEXT NOT NULL DEFAULT '', | ||
| 436 | transports TEXT NOT NULL DEFAULT '[]', | ||
| 437 | flags INTEGER NOT NULL DEFAULT 0, | ||
| 438 | aaguid BLOB, | ||
| 439 | sign_count INTEGER NOT NULL DEFAULT 0, | ||
| 440 | clone_warning INTEGER NOT NULL DEFAULT 0, | ||
| 441 | attachment TEXT NOT NULL DEFAULT '', | ||
| 442 | label TEXT NOT NULL DEFAULT '', | ||
| 443 | created INTEGER NOT NULL, | ||
| 444 | last_used INTEGER NOT NULL DEFAULT 0 | ||
| 445 | ); | ||
| 446 | CREATE TABLE user_invites ( | ||
| 447 | token_hash TEXT PRIMARY KEY, | ||
| 448 | user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 449 | created INTEGER NOT NULL, | ||
| 450 | expires INTEGER NOT NULL, | ||
| 451 | used_at INTEGER, | ||
| 452 | created_by TEXT NOT NULL DEFAULT '' | ||
| 453 | ); | ||
| 454 | CREATE TABLE sessions ( | ||
| 455 | token_hash TEXT PRIMARY KEY, | ||
| 456 | account_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, | ||
| 457 | credential_id TEXT REFERENCES user_credentials(credential_id) ON DELETE CASCADE, | ||
| 458 | created INTEGER NOT NULL, | ||
| 459 | last_seen INTEGER NOT NULL, | ||
| 460 | expires INTEGER NOT NULL, | ||
| 461 | user_agent TEXT NOT NULL DEFAULT '' | ||
| 462 | ); | ||
| 463 | INSERT INTO users(id, name, display, status, created) VALUES('01M5JR2JK29KKMAA1T9MDNTPBP', 'wilbur', 'Wilbur Wright', 'active', 0); | ||
| 489 | 464 | INSERT INTO user_names(name, user_id) VALUES('wilbur', '01M5JR2JK29KKMAA1T9MDNTPBP'); | |
| 490 | INSERT INTO user_names(name, user_id) VALUES('robert', '01M5JR2JK29KKMAA1T9MDNTPBP');`); err != nil { | ||
| 465 | INSERT INTO user_names(name, user_id) VALUES('robert', '01M5JR2JK29KKMAA1T9MDNTPBP'); | ||
| 466 | INSERT INTO instance_admins(user_id) VALUES('01M5JR2JK29KKMAA1T9MDNTPBP'); | ||
| 467 | INSERT INTO user_credentials(credential_id, user_id, public_key, created) VALUES('cred-1', '01M5JR2JK29KKMAA1T9MDNTPBP', x'00', 0); | ||
| 468 | INSERT INTO user_invites(token_hash, user_id, created, expires) VALUES('` + hashToken("tok") + `', '01M5JR2JK29KKMAA1T9MDNTPBP', 0, 9999999999); | ||
| 469 | INSERT INTO sessions(token_hash, account_id, credential_id, created, last_seen, expires) VALUES('sess-1', '01M5JR2JK29KKMAA1T9MDNTPBP', 'cred-1', 0, 0, 9999999999); | ||
| 470 | CREATE TABLE repo_acl ( | ||
| 471 | repo_id TEXT PRIMARY KEY, | ||
| 472 | acl TEXT NOT NULL, | ||
| 473 | updated INTEGER NOT NULL, | ||
| 474 | updated_by TEXT NOT NULL DEFAULT '' | ||
| 475 | ); | ||
| 476 | INSERT INTO repo_acl(repo_id, acl, updated, updated_by) VALUES('repo-1', | ||
| 477 | '{"version":1,"default":"none","entries":{"01M5JR2JK29KKMAA1T9MDNTPBP":"admin"},"runners":["01M5JR2JK29KKMAA1T9MDNTPBP"]}', | ||
| 478 | 0, '01M5JR2JK29KKMAA1T9MDNTPBP'); | ||
| 479 | CREATE TABLE lfs_locks ( | ||
| 480 | id TEXT PRIMARY KEY, | ||
| 481 | repo_id TEXT NOT NULL, | ||
| 482 | path TEXT NOT NULL, | ||
| 483 | ref TEXT NOT NULL DEFAULT '', | ||
| 484 | owner TEXT NOT NULL, | ||
| 485 | locked_at INTEGER NOT NULL | ||
| 486 | ); | ||
| 487 | INSERT INTO lfs_locks(id, repo_id, path, owner, locked_at) VALUES('lock-1', 'repo-1', 'art.psd', '01M5JR2JK29KKMAA1T9MDNTPBP', 0);`); err != nil { | ||
| 488 | t.Fatal(err) | ||
| 489 | } | ||
| 490 | if _, err := db.Exec(`INSERT INTO user_keys(fingerprint, user_id, type, line) VALUES(?,?,?,?)`, | ||
| 491 | keyFP, "01M5JR2JK29KKMAA1T9MDNTPBP", "ssh-ed25519", keyLine); err != nil { | ||
| 491 | 492 | t.Fatal(err) | |
| 492 | 493 | } | |
| 493 | 494 | if err := db.Close(); err != nil { | |
| @@ -503,8 +504,31 @@ INSERT INTO user_names(name, user_id) VALUES('robert', '01M5JR2JK29KKMAA1T9MDNTP | |||
| 503 | 504 | if err != nil { | |
| 504 | 505 | t.Fatal(err) | |
| 505 | 506 | } | |
| 506 | if a.Display != "" || a.Label() != "wilbur" { | ||
| 507 | t.Fatalf("upgraded account display = %q label = %q", a.Display, a.Label()) | ||
| 507 | if a.Name != "wilbur" || !a.Admin || len(a.Keys) != 1 { | ||
| 508 | t.Fatalf("upgraded account = %+v", a) | ||
| 509 | } | ||
| 510 | if name, ok := s.Lookup(a.Keys[0].Fingerprint); !ok || name != "wilbur" { | ||
| 511 | t.Fatalf("upgraded key resolves to %q,%v", name, ok) | ||
| 512 | } | ||
| 513 | if creds, err := s.Credentials("wilbur"); err != nil || len(creds) != 1 { | ||
| 514 | t.Fatalf("upgraded credentials = %v, err = %v", creds, err) | ||
| 515 | } | ||
| 516 | if inviter, err := s.InviteAccount("tok"); err != nil || inviter.Name != "wilbur" { | ||
| 517 | t.Fatalf("upgraded invite = %v, err = %v", inviter, err) | ||
| 518 | } | ||
| 519 | var sessions int | ||
| 520 | if err := s.db.QueryRow(`SELECT count(*) FROM sessions WHERE user_name='wilbur'`).Scan(&sessions); err != nil || sessions != 1 { | ||
| 521 | t.Fatalf("upgraded sessions = %d, err = %v", sessions, err) | ||
| 522 | } | ||
| 523 | var acl, aclUpdatedBy, lockOwner string | ||
| 524 | if err := s.db.QueryRow(`SELECT acl, updated_by FROM repo_acl WHERE repo_id='repo-1'`).Scan(&acl, &aclUpdatedBy); err != nil { | ||
| 525 | t.Fatal(err) | ||
| 526 | } | ||
| 527 | if !strings.Contains(acl, `"wilbur"`) || strings.Contains(acl, "01M5JR2JK29") || aclUpdatedBy != "wilbur" { | ||
| 528 | t.Fatalf("upgraded ACL = %s (updated_by %q)", acl, aclUpdatedBy) | ||
| 529 | } | ||
| 530 | if err := s.db.QueryRow(`SELECT owner FROM lfs_locks WHERE id='lock-1'`).Scan(&lockOwner); err != nil || lockOwner != "wilbur" { | ||
| 531 | t.Fatalf("upgraded lock owner = %q, err = %v", lockOwner, err) | ||
| 508 | 532 | } | |
| 509 | 533 | // The former name was only ever an alias, so it no longer resolves. | |
| 510 | 534 | if _, err := s.Find("robert"); err == nil { | |
| @@ -526,7 +550,7 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 526 | 550 | t.Parallel() | |
| 527 | 551 | s := newStore(t) | |
| 528 | 552 | line, fp := pubLine(t) | |
| 529 | a, err := s.Create("keenan", "Keenan O'Brien", line, "test") | ||
| 553 | a, err := s.Create("keenan", line, "test") | ||
| 530 | 554 | if err != nil { | |
| 531 | 555 | t.Fatal(err) | |
| 532 | 556 | } | |
| @@ -540,15 +564,12 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 540 | 564 | t.Fatal(err) | |
| 541 | 565 | } | |
| 542 | 566 | defer other.Close() | |
| 543 | if got, ok := other.ResolveID("keenan"); !ok || got != a.ID { | ||
| 544 | t.Fatalf("copied roster resolves keenan to %q,%v; want %s", got, ok, a.ID) | ||
| 567 | if got, err := other.Find("keenan"); err != nil || got.Name != a.Name { | ||
| 568 | t.Fatalf("copied roster does not resolve keenan: %v", err) | ||
| 545 | 569 | } | |
| 546 | if _, _, ok := other.Lookup(fp); !ok { | ||
| 570 | if name, ok := other.Lookup(fp); !ok || name != a.Name { | ||
| 547 | 571 | t.Fatal("copied roster lost the key") | |
| 548 | 572 | } | |
| 549 | if got := other.Label(a.ID); got != "Keenan O'Brien" { | ||
| 550 | t.Fatalf("copied roster display = %q", got) | ||
| 551 | } | ||
| 552 | 573 | ||
| 553 | 574 | for _, absent := range []string{"system.git", "trusted_keys"} { | |
| 554 | 575 | if _, err := os.Stat(filepath.Join(filepath.Dir(s.Path()), absent)); err == nil { | |
| @@ -561,13 +582,13 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 561 | 582 | t.Parallel() | |
| 562 | 583 | s := newStore(t) | |
| 563 | 584 | alice := mustCreateBy(t, s, "alice", "operator") | |
| 564 | if s.IsAdmin(alice.ID) { | ||
| 585 | if s.IsAdmin(alice.Name) { | ||
| 565 | 586 | t.Fatal("a new account is an instance admin") | |
| 566 | 587 | } | |
| 567 | 588 | if _, err := s.SetAdmin("alice", true, "operator"); err != nil { | |
| 568 | 589 | t.Fatal(err) | |
| 569 | 590 | } | |
| 570 | if !s.IsAdmin(alice.ID) { | ||
| 591 | if !s.IsAdmin(alice.Name) { | ||
| 571 | 592 | t.Fatal("alice was not made an instance admin") | |
| 572 | 593 | } | |
| 573 | 594 | if a, err := s.Find("alice"); err != nil || !a.Admin { | |
| @@ -581,7 +602,7 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 581 | 602 | t.Fatalf("admin audit line does not name the actor:\n%s", log) | |
| 582 | 603 | } | |
| 583 | 604 | bobKey, _ := pubLine(t) | |
| 584 | if _, err := s.Create("bob", "", bobKey, "operator"); err != nil { | ||
| 605 | if _, err := s.Create("bob", bobKey, "operator"); err != nil { | ||
| 585 | 606 | t.Fatal(err) | |
| 586 | 607 | } | |
| 587 | 608 | if _, err := s.SetAdmin("bob", true, "operator"); err != nil { | |
| @@ -590,7 +611,7 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 590 | 611 | if _, err := s.SetAdmin("alice", false, "operator"); err != nil { | |
| 591 | 612 | t.Fatal(err) | |
| 592 | 613 | } | |
| 593 | if s.IsAdmin(alice.ID) { | ||
| 614 | if s.IsAdmin(alice.Name) { | ||
| 594 | 615 | t.Fatal("admin grant survived revocation") | |
| 595 | 616 | } | |
| 596 | 617 | } | |
| @@ -600,22 +621,22 @@ func TestLastInstanceAdminCannotBeRemoved(t *testing.T) { | |||
| 600 | 621 | s := newStore(t) | |
| 601 | 622 | alice := mustCreateBy(t, s, "alice", "operator") | |
| 602 | 623 | bob := mustCreateBy(t, s, "bob", "operator") | |
| 603 | if _, err := s.SetAdmin(alice.ID, true, "operator"); err != nil { | ||
| 624 | if _, err := s.SetAdmin(alice.Name, true, "operator"); err != nil { | ||
| 604 | 625 | t.Fatal(err) | |
| 605 | 626 | } | |
| 606 | if _, err := s.SetAdmin(alice.ID, false, "operator"); err == nil { | ||
| 627 | if _, err := s.SetAdmin(alice.Name, false, "operator"); err == nil { | ||
| 607 | 628 | t.Fatal("revoked the last instance admin") | |
| 608 | 629 | } | |
| 609 | if _, err := s.Retire(alice.ID, "operator"); err == nil { | ||
| 630 | if _, err := s.Retire(alice.Name, "operator"); err == nil { | ||
| 610 | 631 | t.Fatal("retired the last instance admin") | |
| 611 | 632 | } | |
| 612 | if _, err := s.SetAdmin(bob.ID, true, "operator"); err != nil { | ||
| 633 | if _, err := s.SetAdmin(bob.Name, true, "operator"); err != nil { | ||
| 613 | 634 | t.Fatal(err) | |
| 614 | 635 | } | |
| 615 | if _, err := s.SetAdmin(alice.ID, false, "operator"); err != nil { | ||
| 636 | if _, err := s.SetAdmin(alice.Name, false, "operator"); err != nil { | ||
| 616 | 637 | t.Fatalf("could not revoke an admin when another remained: %v", err) | |
| 617 | 638 | } | |
| 618 | if _, err := s.Retire(bob.ID, "operator"); err == nil { | ||
| 639 | if _, err := s.Retire(bob.Name, "operator"); err == nil { | ||
| 619 | 640 | t.Fatal("retired the last instance admin after revoking the other") | |
| 620 | 641 | } | |
| 621 | 642 | } | |