Artifact a8fc732c733e0ee00fdd4426a4721b06b27aca06efab073c345e2736ecd50b5c:
- File
internal/oltp/account_test.go
— part of check-in
[bf5ba9d2fc]
at
2026-08-16 17:07:31
on branch trunk
— oltp: accounts and per-repo roles
0001 §8's v2 tables. Accounts are the web/API identity (argon2id verifier, site-admin flag, read/write/admin per repo); the RFC §6.3 shared secret stays the sync credential, since the protocol fixes its derivation and the server must recompute it.
ops.Auth becomes the required role, so the Admin level that was already declared now means something. The gate stays open until the first account exists, mirroring the sync-user policy, so existing deployments and local single-user serving are unaffected. (user: dmestas, size: 5219) [annotate] [blame] [check-ins using]
package oltp import ( "path/filepath" "testing" ) func testDB(t *testing.T) *DB { t.Helper() db, err := Open(filepath.Join(t.TempDir(), "oltp.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } func TestPasswordRoundTrip(t *testing.T) { h, err := HashPassword("correct horse battery staple") if err != nil { t.Fatal(err) } if !VerifyPassword(h, "correct horse battery staple") { t.Fatal("correct password rejected") } if VerifyPassword(h, "wrong") { t.Fatal("wrong password accepted") } // Salted: the same password twice must not produce the same verifier, // or the store leaks which accounts share a password. h2, _ := HashPassword("correct horse battery staple") if h == h2 { t.Fatal("verifier is not salted") } } func TestVerifyRejectsMalformed(t *testing.T) { for _, bad := range []string{ "", "not-a-hash", "$argon2id$", "$argon2i$v=19$m=1,t=1,p=1$AAAA$AAAA", "$argon2id$v=99$m=65536,t=1,p=4$AAAA$AAAA", "$argon2id$v=19$m=65536,t=1,p=4$!!!$AAAA", } { if VerifyPassword(bad, "anything") { t.Errorf("accepted malformed verifier %q", bad) } } } func TestAccountLifecycle(t *testing.T) { db := testDB(t) // The gate is open until the first account exists — local single-user // serving keeps working, exactly as sync users do. if db.HasAccounts() { t.Fatal("fresh database reports accounts") } if err := db.AddAccount("dan", "hunter2", false); err != nil { t.Fatal(err) } if !db.HasAccounts() { t.Fatal("account added but gate still open") } if err := db.AddAccount("dan", "other", false); err == nil { t.Fatal("duplicate account name accepted — that is an account takeover") } if _, ok := db.Authenticate("dan", "hunter2"); !ok { t.Fatal("correct credentials rejected") } if _, ok := db.Authenticate("dan", "wrong"); ok { t.Fatal("wrong password accepted") } if _, ok := db.Authenticate("nobody", "hunter2"); ok { t.Fatal("nonexistent account authenticated") } if err := db.SetPassword("dan", "hunter3"); err != nil { t.Fatal(err) } if _, ok := db.Authenticate("dan", "hunter2"); ok { t.Fatal("old password still works after change") } if _, ok := db.Authenticate("dan", "hunter3"); !ok { t.Fatal("new password rejected") } if err := db.SetPassword("ghost", "x"); err == nil { t.Fatal("set password on a nonexistent account") } } func TestRolesAreOrdered(t *testing.T) { db := testDB(t) if err := db.AddAccount("dan", "pw", false); err != nil { t.Fatal(err) } acct, _ := db.Authenticate("dan", "pw") if got := db.RoleOf(acct, "quarry"); got != NoRole { t.Fatalf("ungranted repo gives %s", got) } if err := db.Grant("dan", "quarry", Write); err != nil { t.Fatal(err) } if got := db.RoleOf(acct, "quarry"); got != Write { t.Fatalf("role = %s, want write", got) } // A grant on one repo says nothing about another. if got := db.RoleOf(acct, "other"); got != NoRole { t.Fatalf("grant leaked to another repo: %s", got) } // Ordering is what authorization compares against. if !(NoRole < Read && Read < Write && Write < RepoAdmin) { t.Fatal("roles are not ordered") } if err := db.Revoke("dan", "quarry"); err != nil { t.Fatal(err) } if got := db.RoleOf(acct, "quarry"); got != NoRole { t.Fatalf("revoked role still %s", got) } if err := db.Grant("ghost", "quarry", Write); err == nil { t.Fatal("granted a role to a nonexistent account") } } func TestSiteAdminHoldsEveryRepo(t *testing.T) { db := testDB(t) if err := db.AddAccount("root", "pw", true); err != nil { t.Fatal(err) } acct, ok := db.Authenticate("root", "pw") if !ok || !acct.Admin { t.Fatalf("admin flag lost: %+v", acct) } // No explicit grant anywhere, yet admin everywhere — that is what // "site admin" means, and it must not need per-repo rows. if got := db.RoleOf(acct, "never-granted"); got != RepoAdmin { t.Fatalf("site admin has %s on an ungranted repo", got) } } func TestDeleteAccountDropsItsRoles(t *testing.T) { db := testDB(t) if err := db.AddAccount("temp", "pw", false); err != nil { t.Fatal(err) } if err := db.Grant("temp", "quarry", Write); err != nil { t.Fatal(err) } if err := db.DeleteAccount("temp"); err != nil { t.Fatal(err) } if _, ok := db.Authenticate("temp", "pw"); ok { t.Fatal("deleted account still authenticates") } // A later account reusing the name must not inherit the old grants. if err := db.AddAccount("temp", "pw", false); err != nil { t.Fatal(err) } acct, _ := db.Authenticate("temp", "pw") if got := db.RoleOf(acct, "quarry"); got != NoRole { t.Fatalf("recreated account inherited %s", got) } } func TestGrantsForListsRepos(t *testing.T) { db := testDB(t) if err := db.AddAccount("dan", "pw", false); err != nil { t.Fatal(err) } for repo, role := range map[string]Role{"a": Read, "b": Write, "c": RepoAdmin} { if err := db.Grant("dan", repo, role); err != nil { t.Fatal(err) } } got, err := db.GrantsFor("dan") if err != nil { t.Fatal(err) } if len(got) != 3 || got["a"] != Read || got["b"] != Write || got["c"] != RepoAdmin { t.Fatalf("grants = %v", got) } if repos := SortedRepos(got); repos[0] != "a" || repos[2] != "c" { t.Fatalf("SortedRepos = %v", repos) } }