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.

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 }