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 · 14d 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/passkey_test.go +32−37
| @@ -36,11 +36,11 @@ func testCredential(t *testing.T, label string) Credential { | |||
| 36 | 36 | func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |
| 37 | 37 | t.Parallel() | |
| 38 | 38 | s := newStore(t) | |
| 39 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 39 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 40 | 40 | if err != nil { | |
| 41 | 41 | t.Fatal(err) | |
| 42 | 42 | } | |
| 43 | if a.ID == "" || len(a.Keys) != 0 { | ||
| 43 | if a.Name == "" || len(a.Keys) != 0 { | ||
| 44 | 44 | t.Fatalf("created account = %+v, want an id and no SSH key", a) | |
| 45 | 45 | } | |
| 46 | 46 | if inv.Token == "" || !inv.Expires.After(time.Now()) { | |
| @@ -50,8 +50,8 @@ func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |||
| 50 | 50 | if err != nil { | |
| 51 | 51 | t.Fatal(err) | |
| 52 | 52 | } | |
| 53 | if got.ID != a.ID || got.Name != "alice" { | ||
| 54 | t.Fatalf("InviteAccount = %+v, want %s", got, a.ID) | ||
| 53 | if got.Name != "alice" { | ||
| 54 | t.Fatalf("InviteAccount = %+v, want %s", got, a.Name) | ||
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | cred := testCredential(t, "laptop") | |
| @@ -59,8 +59,8 @@ func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |||
| 59 | 59 | if err != nil { | |
| 60 | 60 | t.Fatal(err) | |
| 61 | 61 | } | |
| 62 | if redeemed.ID != a.ID { | ||
| 63 | t.Fatalf("redeemed account = %s, want %s", redeemed.ID, a.ID) | ||
| 62 | if redeemed.Name != a.Name { | ||
| 63 | t.Fatalf("redeemed account = %s, want %s", redeemed.Name, a.Name) | ||
| 64 | 64 | } | |
| 65 | 65 | creds, err := s.Credentials("alice") | |
| 66 | 66 | if err != nil { | |
| @@ -83,8 +83,8 @@ func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |||
| 83 | 83 | if err != nil { | |
| 84 | 84 | t.Fatal(err) | |
| 85 | 85 | } | |
| 86 | if owner.ID != a.ID || string(found.ID) != string(cred.ID) { | ||
| 87 | t.Fatalf("FindCredential = %s/%x, want %s/%x", owner.ID, found.ID, a.ID, cred.ID) | ||
| 86 | if owner.Name != a.Name || string(found.ID) != string(cred.ID) { | ||
| 87 | t.Fatalf("FindCredential = %s/%x, want %s/%x", owner.Name, found.ID, a.Name, cred.ID) | ||
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | 90 | // The link is spent, including a second attempt with a fresh credential. | |
| @@ -99,7 +99,7 @@ func TestCreateWithInviteAndAcceptPasskey(t *testing.T) { | |||
| 99 | 99 | func TestNewInviteSupersedesTheOldOne(t *testing.T) { | |
| 100 | 100 | t.Parallel() | |
| 101 | 101 | s := newStore(t) | |
| 102 | _, first, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 102 | _, first, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 103 | 103 | if err != nil { | |
| 104 | 104 | t.Fatal(err) | |
| 105 | 105 | } | |
| @@ -118,7 +118,7 @@ func TestNewInviteSupersedesTheOldOne(t *testing.T) { | |||
| 118 | 118 | func TestExpiredInviteIsInvalid(t *testing.T) { | |
| 119 | 119 | t.Parallel() | |
| 120 | 120 | s := newStore(t) | |
| 121 | _, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 121 | _, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 122 | 122 | if err != nil { | |
| 123 | 123 | t.Fatal(err) | |
| 124 | 124 | } | |
| @@ -137,7 +137,7 @@ func TestExpiredInviteIsInvalid(t *testing.T) { | |||
| 137 | 137 | func TestPasskeyOnlyAccountSurvivesReopen(t *testing.T) { | |
| 138 | 138 | t.Parallel() | |
| 139 | 139 | s := newStore(t) | |
| 140 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 140 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 141 | 141 | if err != nil { | |
| 142 | 142 | t.Fatal(err) | |
| 143 | 143 | } | |
| @@ -150,7 +150,7 @@ func TestPasskeyOnlyAccountSurvivesReopen(t *testing.T) { | |||
| 150 | 150 | t.Fatal(err) | |
| 151 | 151 | } | |
| 152 | 152 | defer reopened.Close() | |
| 153 | creds, err := reopened.Credentials(a.ID) | ||
| 153 | creds, err := reopened.Credentials(a.Name) | ||
| 154 | 154 | if err != nil { | |
| 155 | 155 | t.Fatal(err) | |
| 156 | 156 | } | |
| @@ -165,10 +165,10 @@ func TestDuplicatePasskeyRejected(t *testing.T) { | |||
| 165 | 165 | alice := mustCreate(t, s, "alice") | |
| 166 | 166 | bob := mustCreate(t, s, "bob") | |
| 167 | 167 | cred := testCredential(t, "laptop") | |
| 168 | if err := s.AddCredential(alice.ID, cred, "test"); err != nil { | ||
| 168 | if err := s.AddCredential(alice.Name, cred, "test"); err != nil { | ||
| 169 | 169 | t.Fatal(err) | |
| 170 | 170 | } | |
| 171 | if err := s.AddCredential(bob.ID, cred, "test"); err == nil { | ||
| 171 | if err := s.AddCredential(bob.Name, cred, "test"); err == nil { | ||
| 172 | 172 | t.Fatal("the same passkey was attached to two accounts") | |
| 173 | 173 | } | |
| 174 | 174 | } | |
| @@ -182,27 +182,27 @@ func TestUpdatePasskeyCounterAndRemove(t *testing.T) { | |||
| 182 | 182 | // nullable and must not break a later read. | |
| 183 | 183 | cred.Authenticator.AAGUID = nil | |
| 184 | 184 | cred.Transport = nil | |
| 185 | if err := s.AddCredential(a.ID, cred, "test"); err != nil { | ||
| 185 | if err := s.AddCredential(a.Name, cred, "test"); err != nil { | ||
| 186 | 186 | t.Fatal(err) | |
| 187 | 187 | } | |
| 188 | 188 | used := time.Now().UTC().Truncate(time.Second) | |
| 189 | 189 | if err := s.UpdateCredential(cred.ID, 42, used); err != nil { | |
| 190 | 190 | t.Fatal(err) | |
| 191 | 191 | } | |
| 192 | creds, err := s.Credentials(a.ID) | ||
| 192 | creds, err := s.Credentials(a.Name) | ||
| 193 | 193 | if err != nil { | |
| 194 | 194 | t.Fatal(err) | |
| 195 | 195 | } | |
| 196 | 196 | if creds[0].Authenticator.SignCount != 42 || !creds[0].LastUsed.Equal(used) { | |
| 197 | 197 | t.Fatalf("counter update lost: %+v", creds[0]) | |
| 198 | 198 | } | |
| 199 | if err := s.RemoveCredential(a.ID, cred.ID, "test"); err != nil { | ||
| 199 | if err := s.RemoveCredential(a.Name, cred.ID, "test"); err != nil { | ||
| 200 | 200 | t.Fatal(err) | |
| 201 | 201 | } | |
| 202 | 202 | if _, _, err := s.FindCredential(cred.ID); !errors.Is(err, ErrCredentialNotFound) { | |
| 203 | 203 | t.Fatalf("removed credential still resolves: %v", err) | |
| 204 | 204 | } | |
| 205 | if err := s.RemoveCredential(a.ID, cred.ID, "test"); !errors.Is(err, ErrCredentialNotFound) { | ||
| 205 | if err := s.RemoveCredential(a.Name, cred.ID, "test"); !errors.Is(err, ErrCredentialNotFound) { | ||
| 206 | 206 | t.Fatalf("removing twice = %v, want ErrCredentialNotFound", err) | |
| 207 | 207 | } | |
| 208 | 208 | } | |
| @@ -210,7 +210,7 @@ func TestUpdatePasskeyCounterAndRemove(t *testing.T) { | |||
| 210 | 210 | func TestRetireEndsInvitesAndSessionsAndHidesCredentials(t *testing.T) { | |
| 211 | 211 | t.Parallel() | |
| 212 | 212 | s := newStore(t) | |
| 213 | a, inv, err := s.CreateWithInvite("alice", "", "operator", time.Hour) | ||
| 213 | a, inv, err := s.CreateWithInvite("alice", "operator", time.Hour) | ||
| 214 | 214 | if err != nil { | |
| 215 | 215 | t.Fatal(err) | |
| 216 | 216 | } | |
| @@ -218,8 +218,8 @@ func TestRetireEndsInvitesAndSessionsAndHidesCredentials(t *testing.T) { | |||
| 218 | 218 | if _, err := s.AcceptInvite(inv.Token, cred, "alice"); err != nil { | |
| 219 | 219 | t.Fatal(err) | |
| 220 | 220 | } | |
| 221 | if _, err := s.db.Exec(`INSERT INTO sessions(token_hash, account_id, credential_id, created, last_seen, expires) | ||
| 222 | VALUES('hash', ?, ?, 1, 1, 9999999999)`, a.ID, credentialIDString(cred.ID)); err != nil { | ||
| 221 | if _, err := s.db.Exec(`INSERT INTO sessions(token_hash, user_name, credential_id, created, last_seen, expires) | ||
| 222 | VALUES('hash', ?, ?, 1, 1, 9999999999)`, a.Name, credentialIDString(cred.ID)); err != nil { | ||
| 223 | 223 | t.Fatal(err) | |
| 224 | 224 | } | |
| 225 | 225 | // An outstanding invite must also die with the account. | |
| @@ -230,13 +230,13 @@ VALUES('hash', ?, ?, 1, 1, 9999999999)`, a.ID, credentialIDString(cred.ID)); err | |||
| 230 | 230 | t.Fatal(err) | |
| 231 | 231 | } | |
| 232 | 232 | var sessions, invites int | |
| 233 | if err := s.db.QueryRow(`SELECT count(*) FROM sessions WHERE account_id=?`, a.ID).Scan(&sessions); err != nil { | ||
| 233 | if err := s.db.QueryRow(`SELECT count(*) FROM sessions WHERE user_name=?`, a.Name).Scan(&sessions); err != nil { | ||
| 234 | 234 | t.Fatal(err) | |
| 235 | 235 | } | |
| 236 | 236 | if sessions != 0 { | |
| 237 | 237 | t.Fatalf("retire left %d session(s)", sessions) | |
| 238 | 238 | } | |
| 239 | if err := s.db.QueryRow(`SELECT count(*) FROM user_invites WHERE user_id=?`, a.ID).Scan(&invites); err != nil { | ||
| 239 | if err := s.db.QueryRow(`SELECT count(*) FROM user_invites WHERE user_name=?`, a.Name).Scan(&invites); err != nil { | ||
| 240 | 240 | t.Fatal(err) | |
| 241 | 241 | } | |
| 242 | 242 | if invites != 0 { | |
| @@ -252,20 +252,20 @@ func TestRemovingPasskeyCascadesItsSessions(t *testing.T) { | |||
| 252 | 252 | s := newStore(t) | |
| 253 | 253 | a := mustCreate(t, s, "alice") | |
| 254 | 254 | cred := testCredential(t, "laptop") | |
| 255 | if err := s.AddCredential(a.ID, cred, "test"); err != nil { | ||
| 255 | if err := s.AddCredential(a.Name, cred, "test"); err != nil { | ||
| 256 | 256 | t.Fatal(err) | |
| 257 | 257 | } | |
| 258 | 258 | other := testCredential(t, "phone") | |
| 259 | if err := s.AddCredential(a.ID, other, "test"); err != nil { | ||
| 259 | if err := s.AddCredential(a.Name, other, "test"); err != nil { | ||
| 260 | 260 | t.Fatal(err) | |
| 261 | 261 | } | |
| 262 | 262 | for _, id := range [][]byte{cred.ID, other.ID} { | |
| 263 | if _, err := s.db.Exec(`INSERT INTO sessions(token_hash, account_id, credential_id, created, last_seen, expires) | ||
| 264 | VALUES(?, ?, ?, 1, 1, 9999999999)`, "hash-"+credentialIDString(id), a.ID, credentialIDString(id)); err != nil { | ||
| 263 | if _, err := s.db.Exec(`INSERT INTO sessions(token_hash, user_name, credential_id, created, last_seen, expires) | ||
| 264 | VALUES(?, ?, ?, 1, 1, 9999999999)`, "hash-"+credentialIDString(id), a.Name, credentialIDString(id)); err != nil { | ||
| 265 | 265 | t.Fatal(err) | |
| 266 | 266 | } | |
| 267 | 267 | } | |
| 268 | if err := s.RemoveCredential(a.ID, cred.ID, "test"); err != nil { | ||
| 268 | if err := s.RemoveCredential(a.Name, cred.ID, "test"); err != nil { | ||
| 269 | 269 | t.Fatal(err) | |
| 270 | 270 | } | |
| 271 | 271 | var n int | |
| @@ -286,26 +286,21 @@ VALUES(?, ?, ?, 1, 1, 9999999999)`, "hash-"+credentialIDString(id), a.ID, creden | |||
| 286 | 286 | func TestCreatePasskeySelfRegistration(t *testing.T) { | |
| 287 | 287 | t.Parallel() | |
| 288 | 288 | s := newStore(t) | |
| 289 | id := "01M5JR2JK29KKMAA1T9MDNTPBP" | ||
| 290 | 289 | cred := testCredential(t, "first passkey") | |
| 291 | a, err := s.CreatePasskey("alice", "", id, cred, "self-registration") | ||
| 290 | a, err := s.CreatePasskey("alice", cred, "self-registration") | ||
| 292 | 291 | if err != nil { | |
| 293 | 292 | t.Fatal(err) | |
| 294 | 293 | } | |
| 295 | if a.ID != id || a.Name != "alice" || len(a.Keys) != 0 { | ||
| 294 | if a.Name != "alice" || len(a.Keys) != 0 { | ||
| 296 | 295 | t.Fatalf("created account = %+v", a) | |
| 297 | 296 | } | |
| 298 | creds, err := s.Credentials(a.ID) | ||
| 297 | creds, err := s.Credentials(a.Name) | ||
| 299 | 298 | if err != nil || len(creds) != 1 || string(creds[0].ID) != string(cred.ID) { | |
| 300 | 299 | t.Fatalf("credentials = %+v, %v", creds, err) | |
| 301 | 300 | } | |
| 302 | 301 | ||
| 303 | 302 | // A second registration for the same name must not leave an account behind. | |
| 304 | other := "01M5JR2JK29KKMAA1T9MDNTPBQ" | ||
| 305 | if _, err := s.CreatePasskey("alice", "", other, testCredential(t, "other"), "self-registration"); err == nil { | ||
| 303 | if _, err := s.CreatePasskey("alice", testCredential(t, "other"), "self-registration"); err == nil { | ||
| 306 | 304 | t.Fatal("duplicate name was accepted") | |
| 307 | 305 | } | |
| 308 | if _, err := s.Find(other); err == nil { | ||
| 309 | t.Fatal("failed registration left an account behind") | ||
| 310 | } | ||
| 311 | 306 | } | |