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 · 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 }