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.
Create accounts with a key and find the instance config
c36d507302c0f7fb24af6eba390c4121d67318c3 · Keenan Nemetz · 16d ago · parent 3abbe749
Create accounts with a key and find the instance config An account is now written with its first key in one transaction, so a name cannot be claimed without a credential. add-key remains for additional keys, and remote user create forwards the key on stdin. Local commands now resolve the config from --config, VOLE_CONFIG, ./vole.toml, or ~/.vole/vole.toml before falling back to defaults. init prints --config in its next steps when the instance will not be discovered automatically.
forge/identity/identity_test.go +123−74
| @@ -41,6 +41,21 @@ func pubLine(t *testing.T) (string, string) { | |||
| 41 | 41 | return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(k))), ssh.FingerprintSHA256(k) | |
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | func mustCreate(t *testing.T, s *Store, name string) *Account { | ||
| 45 | t.Helper() | ||
| 46 | return mustCreateBy(t, s, name, "test") | ||
| 47 | } | ||
| 48 | |||
| 49 | func mustCreateBy(t *testing.T, s *Store, name, actor string) *Account { | ||
| 50 | t.Helper() | ||
| 51 | line, _ := pubLine(t) | ||
| 52 | a, err := s.Create(name, line, actor) | ||
| 53 | if err != nil { | ||
| 54 | t.Fatalf("create %s: %v", name, err) | ||
| 55 | } | ||
| 56 | return a | ||
| 57 | } | ||
| 58 | |||
| 44 | 59 | func TestEmptyRosterLoads(t *testing.T) { | |
| 45 | 60 | t.Parallel() | |
| 46 | 61 | s := newStore(t) | |
| @@ -56,41 +71,95 @@ func TestMissingDatabaseIsAnError(t *testing.T) { | |||
| 56 | 71 | } | |
| 57 | 72 | } | |
| 58 | 73 | ||
| 59 | func TestCreateAccountAndAddKey(t *testing.T) { | ||
| 74 | func TestCreateAccountWithKeyAndAddKey(t *testing.T) { | ||
| 60 | 75 | t.Parallel() | |
| 61 | 76 | s := newStore(t) | |
| 62 | a, err := s.Create("keenan", "test") | ||
| 77 | line, wantFP := pubLine(t) | ||
| 78 | a, err := s.Create("keenan", line, "test") | ||
| 63 | 79 | if err != nil { | |
| 64 | 80 | t.Fatal(err) | |
| 65 | 81 | } | |
| 66 | 82 | if !id.Valid(a.ID) { | |
| 67 | 83 | t.Fatalf("account id %q is not a ULID", a.ID) | |
| 68 | 84 | } | |
| 69 | line, wantFP := pubLine(t) | ||
| 70 | fp, err := s.AddKey("keenan", line, "test") | ||
| 71 | if err != nil { | ||
| 72 | t.Fatal(err) | ||
| 73 | } | ||
| 74 | if fp != wantFP { | ||
| 75 | t.Fatalf("fingerprint = %s, want %s", fp, wantFP) | ||
| 85 | if len(a.Keys) != 1 || a.Keys[0].Fingerprint != wantFP { | ||
| 86 | t.Fatalf("created account keys = %+v, want the supplied key %s", a.Keys, wantFP) | ||
| 76 | 87 | } | |
| 77 | accountID, name, ok := s.Lookup(fp) | ||
| 88 | accountID, name, ok := s.Lookup(wantFP) | ||
| 78 | 89 | if !ok || accountID != a.ID || name != "keenan" { | |
| 79 | 90 | t.Fatalf("Lookup = %q,%q,%v; want %q,keenan,true", accountID, name, ok, a.ID) | |
| 80 | 91 | } | |
| 81 | 92 | if got, ok := s.ResolveID("keenan"); !ok || got != a.ID { | |
| 82 | 93 | t.Fatalf("ResolveID = %q,%v", got, ok) | |
| 83 | 94 | } | |
| 95 | |||
| 96 | // add-key remains the way to attach a second credential. | ||
| 97 | second, secondFP := pubLine(t) | ||
| 98 | fp, err := s.AddKey("keenan", second, "test") | ||
| 99 | if err != nil { | ||
| 100 | t.Fatal(err) | ||
| 101 | } | ||
| 102 | if fp != secondFP { | ||
| 103 | t.Fatalf("added fingerprint = %s, want %s", fp, secondFP) | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | // TestCreateRequiresAKeyAndDoesNotReserveTheName: a failed attempt must leave | ||
| 108 | // neither an account nor a name reservation behind, or open registration could | ||
| 109 | // claim names without presenting a credential. | ||
| 110 | func TestCreateRequiresAKeyAndDoesNotReserveTheName(t *testing.T) { | ||
| 111 | t.Parallel() | ||
| 112 | s := newStore(t) | ||
| 113 | for _, bad := range []string{"", "not an SSH key"} { | ||
| 114 | if _, err := s.Create("alice", bad, "test"); err == nil { | ||
| 115 | t.Fatalf("Create accepted key %q", bad) | ||
| 116 | } | ||
| 117 | } | ||
| 118 | if _, err := s.Find("alice"); err == nil { | ||
| 119 | t.Fatal("a failed create left an account behind") | ||
| 120 | } | ||
| 121 | line, _ := pubLine(t) | ||
| 122 | a, err := s.Create("alice", line, "test") | ||
| 123 | if err != nil { | ||
| 124 | t.Fatalf("the failed attempts reserved the name: %v", err) | ||
| 125 | } | ||
| 126 | if got := len(a.Keys); got != 1 { | ||
| 127 | t.Fatalf("keys = %d, want 1", got) | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | // TestCreateRejectsAKeyAlreadyRegistered: two accounts cannot share a key, and | ||
| 132 | // the second account must not be created at all. | ||
| 133 | func TestCreateRejectsAKeyAlreadyRegistered(t *testing.T) { | ||
| 134 | t.Parallel() | ||
| 135 | s := newStore(t) | ||
| 136 | line, _ := pubLine(t) | ||
| 137 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 138 | t.Fatal(err) | ||
| 139 | } | ||
| 140 | if _, err := s.Create("bob", line, "test"); err == nil { | ||
| 141 | t.Fatal("a registered key was accepted for a second account") | ||
| 142 | } else if !strings.Contains(err.Error(), "already registered") { | ||
| 143 | t.Fatalf("unclear error: %v", err) | ||
| 144 | } | ||
| 145 | if _, err := s.Find("bob"); err == nil { | ||
| 146 | t.Fatal("the duplicate-key failure left an account behind") | ||
| 147 | } | ||
| 148 | fresh, _ := pubLine(t) | ||
| 149 | if _, err := s.Create("bob", fresh, "test"); err != nil { | ||
| 150 | t.Fatalf("the duplicate-key failure reserved bob's name: %v", err) | ||
| 151 | } | ||
| 84 | 152 | } | |
| 85 | 153 | ||
| 86 | 154 | func TestMultipleKeysPerAccount(t *testing.T) { | |
| 87 | 155 | t.Parallel() | |
| 88 | 156 | s := newStore(t) | |
| 89 | if _, err := s.Create("keenan", "test"); err != nil { | ||
| 157 | line, _ := pubLine(t) | ||
| 158 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 90 | 159 | t.Fatal(err) | |
| 91 | 160 | } | |
| 92 | 161 | var fps []string | |
| 93 | for i := 0; i < 3; i++ { | ||
| 162 | for i := 0; i < 2; i++ { | ||
| 94 | 163 | line, fp := pubLine(t) | |
| 95 | 164 | if _, err := s.AddKey("keenan", line, "test"); err != nil { | |
| 96 | 165 | t.Fatal(err) | |
| @@ -118,13 +187,12 @@ func TestMultipleKeysPerAccount(t *testing.T) { | |||
| 118 | 187 | func TestDuplicateKeyAcrossAccountsRejected(t *testing.T) { | |
| 119 | 188 | t.Parallel() | |
| 120 | 189 | s := newStore(t) | |
| 121 | for _, n := range []string{"alice", "bob"} { | ||
| 122 | if _, err := s.Create(n, "test"); err != nil { | ||
| 123 | t.Fatal(err) | ||
| 124 | } | ||
| 125 | } | ||
| 126 | 190 | line, _ := pubLine(t) | |
| 127 | if _, err := s.AddKey("alice", line, "test"); err != nil { | ||
| 191 | if _, err := s.Create("alice", line, "test"); err != nil { | ||
| 192 | t.Fatal(err) | ||
| 193 | } | ||
| 194 | bobLine, _ := pubLine(t) | ||
| 195 | if _, err := s.Create("bob", bobLine, "test"); err != nil { | ||
| 128 | 196 | t.Fatal(err) | |
| 129 | 197 | } | |
| 130 | 198 | if _, err := s.AddKey("bob", line, "test"); err == nil { | |
| @@ -140,11 +208,8 @@ func TestDuplicateKeyAcrossAccountsRejected(t *testing.T) { | |||
| 140 | 208 | func TestOneKeyBelongsToOneAccount(t *testing.T) { | |
| 141 | 209 | t.Parallel() | |
| 142 | 210 | s := newStore(t) | |
| 143 | for _, n := range []string{"alice", "bob"} { | ||
| 144 | if _, err := s.Create(n, "test"); err != nil { | ||
| 145 | t.Fatal(err) | ||
| 146 | } | ||
| 147 | } | ||
| 211 | mustCreate(t, s, "alice") | ||
| 212 | mustCreate(t, s, "bob") | ||
| 148 | 213 | line, fp := pubLine(t) | |
| 149 | 214 | if _, err := s.AddKey("alice", line, "test"); err != nil { | |
| 150 | 215 | t.Fatal(err) | |
| @@ -170,9 +235,7 @@ func TestOneKeyBelongsToOneAccount(t *testing.T) { | |||
| 170 | 235 | func TestCorruptRosterFailsStartup(t *testing.T) { | |
| 171 | 236 | t.Parallel() | |
| 172 | 237 | s := newStore(t) | |
| 173 | if _, err := s.Create("alice", "test"); err != nil { | ||
| 174 | t.Fatal(err) | ||
| 175 | } | ||
| 238 | mustCreate(t, s, "alice") | ||
| 176 | 239 | raw, err := openDB(s.Path()) | |
| 177 | 240 | if err != nil { | |
| 178 | 241 | t.Fatal(err) | |
| @@ -195,15 +258,13 @@ func TestCorruptRosterFailsStartup(t *testing.T) { | |||
| 195 | 258 | func TestRemoveKey(t *testing.T) { | |
| 196 | 259 | t.Parallel() | |
| 197 | 260 | s := newStore(t) | |
| 198 | if _, err := s.Create("keenan", "test"); err != nil { | ||
| 199 | t.Fatal(err) | ||
| 200 | } | ||
| 201 | 261 | keepLine, keepFP := pubLine(t) | |
| 202 | 262 | dropLine, dropFP := pubLine(t) | |
| 203 | for _, l := range []string{keepLine, dropLine} { | ||
| 204 | if _, err := s.AddKey("keenan", l, "test"); err != nil { | ||
| 205 | t.Fatal(err) | ||
| 206 | } | ||
| 263 | if _, err := s.Create("keenan", keepLine, "test"); err != nil { | ||
| 264 | t.Fatal(err) | ||
| 265 | } | ||
| 266 | if _, err := s.AddKey("keenan", dropLine, "test"); err != nil { | ||
| 267 | t.Fatal(err) | ||
| 207 | 268 | } | |
| 208 | 269 | if err := s.RemoveKey("keenan", dropFP, "test"); err != nil { | |
| 209 | 270 | t.Fatal(err) | |
| @@ -221,12 +282,9 @@ func TestRemoveKey(t *testing.T) { | |||
| 221 | 282 | func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |
| 222 | 283 | t.Parallel() | |
| 223 | 284 | s := newStore(t) | |
| 224 | a, err := s.Create("wilbur", "test") | ||
| 225 | if err != nil { | ||
| 226 | t.Fatal(err) | ||
| 227 | } | ||
| 228 | 285 | line, fp := pubLine(t) | |
| 229 | if _, err := s.AddKey("wilbur", line, "test"); err != nil { | ||
| 286 | a, err := s.Create("wilbur", line, "test") | ||
| 287 | if err != nil { | ||
| 230 | 288 | t.Fatal(err) | |
| 231 | 289 | } | |
| 232 | 290 | if _, err := s.Retire("wilbur", "test"); err != nil { | |
| @@ -238,7 +296,8 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 238 | 296 | if _, ok := s.ResolveID("wilbur"); ok { | |
| 239 | 297 | t.Fatal("a retired name still resolves") | |
| 240 | 298 | } | |
| 241 | if _, err := s.Create("wilbur", "test"); err == nil { | ||
| 299 | reissue, _ := pubLine(t) | ||
| 300 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 242 | 301 | t.Fatalf("the retired name was reissued to a new account") | |
| 243 | 302 | } else if !strings.Contains(err.Error(), "cannot be reused") { | |
| 244 | 303 | t.Fatalf("unclear error: %v", err) | |
| @@ -254,12 +313,9 @@ func TestRetireStopsAuthenticationAndReservesTheName(t *testing.T) { | |||
| 254 | 313 | func TestRenameKeepsTheIdAndReservesBothNames(t *testing.T) { | |
| 255 | 314 | t.Parallel() | |
| 256 | 315 | s := newStore(t) | |
| 257 | a, err := s.Create("wilbur", "test") | ||
| 258 | if err != nil { | ||
| 259 | t.Fatal(err) | ||
| 260 | } | ||
| 261 | 316 | line, fp := pubLine(t) | |
| 262 | if _, err := s.AddKey("wilbur", line, "test"); err != nil { | ||
| 317 | a, err := s.Create("wilbur", line, "test") | ||
| 318 | if err != nil { | ||
| 263 | 319 | t.Fatal(err) | |
| 264 | 320 | } | |
| 265 | 321 | renamed, err := s.Rename("wilbur", "robert", "test") | |
| @@ -279,10 +335,12 @@ func TestRenameKeepsTheIdAndReservesBothNames(t *testing.T) { | |||
| 279 | 335 | t.Fatalf("new name does not resolve: %q,%v", got, ok) | |
| 280 | 336 | } | |
| 281 | 337 | // Both names stay reserved. | |
| 282 | if _, err := s.Create("wilbur", "test"); err == nil { | ||
| 338 | reissue, _ := pubLine(t) | ||
| 339 | if _, err := s.Create("wilbur", reissue, "test"); err == nil { | ||
| 283 | 340 | t.Fatal("the former name was handed to a new account") | |
| 284 | 341 | } | |
| 285 | other, err := s.Create("dana", "test") | ||
| 342 | otherLine, _ := pubLine(t) | ||
| 343 | other, err := s.Create("dana", otherLine, "test") | ||
| 286 | 344 | if err != nil { | |
| 287 | 345 | t.Fatal(err) | |
| 288 | 346 | } | |
| @@ -298,8 +356,9 @@ func TestRenameKeepsTheIdAndReservesBothNames(t *testing.T) { | |||
| 298 | 356 | func TestInvalidNamesRefused(t *testing.T) { | |
| 299 | 357 | t.Parallel() | |
| 300 | 358 | s := newStore(t) | |
| 359 | line, _ := pubLine(t) | ||
| 301 | 360 | for _, name := range []string{"Keenan", "has space", "a", "with/slash", "trailing."} { | |
| 302 | if _, err := s.Create(name, "test"); err == nil { | ||
| 361 | if _, err := s.Create(name, line, "test"); err == nil { | ||
| 303 | 362 | t.Fatalf("%q accepted as a name", name) | |
| 304 | 363 | } | |
| 305 | 364 | } | |
| @@ -310,17 +369,17 @@ func TestInvalidNamesRefused(t *testing.T) { | |||
| 310 | 369 | func TestConcurrentAddsAllLand(t *testing.T) { | |
| 311 | 370 | t.Parallel() | |
| 312 | 371 | s := newStore(t) | |
| 313 | if _, err := s.Create("keenan", "test"); err != nil { | ||
| 314 | t.Fatal(err) | ||
| 315 | } | ||
| 316 | 372 | const n = 8 | |
| 317 | 373 | lines := make([]string, n) | |
| 318 | 374 | for i := range lines { | |
| 319 | 375 | lines[i], _ = pubLine(t) | |
| 320 | 376 | } | |
| 377 | if _, err := s.Create("keenan", lines[0], "test"); err != nil { | ||
| 378 | t.Fatal(err) | ||
| 379 | } | ||
| 321 | 380 | var wg sync.WaitGroup | |
| 322 | 381 | errs := make([]error, n) | |
| 323 | for i := 0; i < n; i++ { | ||
| 382 | for i := 1; i < n; i++ { | ||
| 324 | 383 | wg.Add(1) | |
| 325 | 384 | go func(i int) { | |
| 326 | 385 | defer wg.Done() | |
| @@ -344,17 +403,18 @@ func TestConcurrentAddsAllLand(t *testing.T) { | |||
| 344 | 403 | func TestAuditLogRecordsChanges(t *testing.T) { | |
| 345 | 404 | t.Parallel() | |
| 346 | 405 | s := newStore(t) | |
| 347 | if _, err := s.Create("keenan", "test"); err != nil { | ||
| 406 | line, fp := pubLine(t) | ||
| 407 | if _, err := s.Create("keenan", line, "test"); err != nil { | ||
| 348 | 408 | t.Fatal(err) | |
| 349 | 409 | } | |
| 350 | line, fp := pubLine(t) | ||
| 351 | if _, err := s.AddKey("keenan", line, "test"); err != nil { | ||
| 410 | second, secondFP := pubLine(t) | ||
| 411 | if _, err := s.AddKey("keenan", second, "test"); err != nil { | ||
| 352 | 412 | t.Fatal(err) | |
| 353 | 413 | } | |
| 354 | 414 | if _, err := s.Rename("keenan", "robert", "test"); err != nil { | |
| 355 | 415 | t.Fatal(err) | |
| 356 | 416 | } | |
| 357 | if err := s.RemoveKey("robert", fp, "test"); err != nil { | ||
| 417 | if err := s.RemoveKey("robert", secondFP, "test"); err != nil { | ||
| 358 | 418 | t.Fatal(err) | |
| 359 | 419 | } | |
| 360 | 420 | data, err := os.ReadFile(s.AuditPath) | |
| @@ -362,7 +422,7 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 362 | 422 | t.Fatal(err) | |
| 363 | 423 | } | |
| 364 | 424 | log := string(data) | |
| 365 | for _, want := range []string{"identity.create", "identity.add-key", "identity.rename", "identity.remove-key", fp} { | ||
| 425 | for _, want := range []string{"identity.create", "identity.add-key", "identity.rename", "identity.remove-key", fp, secondFP} { | ||
| 366 | 426 | if !strings.Contains(log, want) { | |
| 367 | 427 | t.Fatalf("audit log missing %q:\n%s", want, log) | |
| 368 | 428 | } | |
| @@ -375,12 +435,9 @@ func TestAuditLogRecordsChanges(t *testing.T) { | |||
| 375 | 435 | func TestRosterIsOneDatabaseFile(t *testing.T) { | |
| 376 | 436 | t.Parallel() | |
| 377 | 437 | s := newStore(t) | |
| 378 | a, err := s.Create("keenan", "test") | ||
| 379 | if err != nil { | ||
| 380 | t.Fatal(err) | ||
| 381 | } | ||
| 382 | 438 | line, fp := pubLine(t) | |
| 383 | if _, err := s.AddKey(a.ID, line, "test"); err != nil { | ||
| 439 | a, err := s.Create("keenan", line, "test") | ||
| 440 | if err != nil { | ||
| 384 | 441 | t.Fatal(err) | |
| 385 | 442 | } | |
| 386 | 443 | if _, err := s.Rename(a.ID, "robert", "test"); err != nil { | |
| @@ -428,10 +485,7 @@ func equal(got, want []string) bool { | |||
| 428 | 485 | func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |
| 429 | 486 | t.Parallel() | |
| 430 | 487 | s := newStore(t) | |
| 431 | alice, err := s.Create("alice", "operator") | ||
| 432 | if err != nil { | ||
| 433 | t.Fatal(err) | ||
| 434 | } | ||
| 488 | alice := mustCreateBy(t, s, "alice", "operator") | ||
| 435 | 489 | if s.IsAdmin(alice.ID) { | |
| 436 | 490 | t.Fatal("a new account is an instance admin") | |
| 437 | 491 | } | |
| @@ -451,7 +505,8 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 451 | 505 | if !strings.Contains(string(log), "identity.grant-admin\toperator\talice") { | |
| 452 | 506 | t.Fatalf("admin audit line does not name the actor:\n%s", log) | |
| 453 | 507 | } | |
| 454 | if _, err := s.Create("bob", "operator"); err != nil { | ||
| 508 | bobKey, _ := pubLine(t) | ||
| 509 | if _, err := s.Create("bob", bobKey, "operator"); err != nil { | ||
| 455 | 510 | t.Fatal(err) | |
| 456 | 511 | } | |
| 457 | 512 | if _, err := s.SetAdmin("bob", true, "operator"); err != nil { | |
| @@ -468,14 +523,8 @@ func TestInstanceAdminsAreExplicitAndAudited(t *testing.T) { | |||
| 468 | 523 | func TestLastInstanceAdminCannotBeRemoved(t *testing.T) { | |
| 469 | 524 | t.Parallel() | |
| 470 | 525 | s := newStore(t) | |
| 471 | alice, err := s.Create("alice", "operator") | ||
| 472 | if err != nil { | ||
| 473 | t.Fatal(err) | ||
| 474 | } | ||
| 475 | bob, err := s.Create("bob", "operator") | ||
| 476 | if err != nil { | ||
| 477 | t.Fatal(err) | ||
| 478 | } | ||
| 526 | alice := mustCreateBy(t, s, "alice", "operator") | ||
| 527 | bob := mustCreateBy(t, s, "bob", "operator") | ||
| 479 | 528 | if _, err := s.SetAdmin(alice.ID, true, "operator"); err != nil { | |
| 480 | 529 | t.Fatal(err) | |
| 481 | 530 | } | |