Skip to content
quarry Artifact [8f1eb8670b]
Login

Artifact [8f1eb8670b]

Artifact 8f1eb8670b10adc190c52487a7e3ba8d097190e760003d01c6d05be520711ba6:


package oltp

import (
	"path/filepath"
	"testing"
	"time"
)

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

// OwnedRepoCount is the creation quota's unit: repositories held by
// EXPLICIT admin grant, not merely readable or writable ones.
func TestOwnedRepoCountCountsAdminGrantsOnly(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "pw", false); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "reads-only", Read); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "writes-only", Write); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "owns-one", RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "owns-two", RepoAdmin); err != nil {
		t.Fatal(err)
	}
	n, err := db.OwnedRepoCount("dan")
	if err != nil {
		t.Fatal(err)
	}
	if n != 2 {
		t.Fatalf("OwnedRepoCount = %d, want 2 (read and write grants must not count)", n)
	}
	if n, err := db.OwnedRepoCount("nobody-such-account"); err != nil || n != 0 {
		t.Fatalf("OwnedRepoCount for an unknown account = %d, %v", n, err)
	}
}

func TestSessionLifecycle(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "hunter2", false); err != nil {
		t.Fatal(err)
	}
	token, expires, ok := db.NewSession("dan", "hunter2")
	if !ok {
		t.Fatal("login with correct credentials failed")
	}
	if expires.Before(time.Now()) {
		t.Fatalf("session already expired: %v", expires)
	}
	acct, ok := db.SessionAccount(token)
	if !ok || acct.Name != "dan" {
		t.Fatalf("resolve = %+v %v", acct, ok)
	}
	if _, _, ok := db.NewSession("dan", "wrong"); ok {
		t.Fatal("login succeeded with the wrong password")
	}
	if _, ok := db.SessionAccount("not-a-token"); ok {
		t.Fatal("an invented token resolved")
	}
	db.EndSession(token)
	if _, ok := db.SessionAccount(token); ok {
		t.Fatal("session survived logout")
	}
}

// The cookie value must not be recoverable from the database. A session
// table that reads back into working cookies turns a backup or a stray
// SELECT into a set of live logins.
func TestSessionTokenIsStoredHashed(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "pw", false); err != nil {
		t.Fatal(err)
	}
	token, _, ok := db.NewSession("dan", "pw")
	if !ok {
		t.Fatal("login failed")
	}
	var stored string
	if err := db.db.QueryRow(`SELECT token FROM session`).Scan(&stored); err != nil {
		t.Fatal(err)
	}
	if stored == token {
		t.Fatal("the raw cookie value is in the database")
	}
	if _, ok := db.SessionAccount(stored); ok {
		t.Fatal("the stored digest works as a cookie")
	}
}

// Changing a password is how somebody responds to a credential they
// believe is compromised; it has to invalidate the browser already
// holding a session, or the response does nothing.
func TestPasswordChangeEndsSessions(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "old", false); err != nil {
		t.Fatal(err)
	}
	token, _, _ := db.NewSession("dan", "old")
	if _, ok := db.SessionAccount(token); !ok {
		t.Fatal("session not established")
	}
	if err := db.SetPassword("dan", "new"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.SessionAccount(token); ok {
		t.Fatal("session survived a password change")
	}
}

// VerifyPassword is Authenticate's verdict without its session —
// exactly the "is this really your current password" check a
// self-service change needs.
func TestVerifyPassword(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "hunter2", false); err != nil {
		t.Fatal(err)
	}
	if !db.VerifyPassword("dan", "hunter2") {
		t.Fatal("the correct password did not verify")
	}
	if db.VerifyPassword("dan", "wrong") {
		t.Fatal("the wrong password verified")
	}
	if db.VerifyPassword("nobody", "anything") {
		t.Fatal("a nonexistent account verified")
	}
}

// SetPasswordSelf ends every OTHER session but spares the one named by
// keep — the browser making the change stays signed in, unlike
// SetPassword's full sweep.
func TestSetPasswordSelfSparesTheNamedSession(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "old", false); err != nil {
		t.Fatal(err)
	}
	mine, _, _ := db.NewSession("dan", "old")
	other, _, _ := db.NewSessionFrom("dan", "old", "", "")
	if err := db.SetPasswordSelf("dan", "new", SessionID(mine)); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.SessionAccount(mine); !ok {
		t.Fatal("the session making the change was ended")
	}
	if _, ok := db.SessionAccount(other); ok {
		t.Fatal("another session survived a self-service password change")
	}
	if !db.VerifyPassword("dan", "new") {
		t.Fatal("the new password does not verify")
	}
}

// keep == "" behaves exactly like the admin-driven SetPassword: every
// session ends, including the one that asked.
func TestSetPasswordSelfWithNoKeepEndsEverything(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "old", false); err != nil {
		t.Fatal(err)
	}
	token, _, _ := db.NewSession("dan", "old")
	if err := db.SetPasswordSelf("dan", "new", ""); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.SessionAccount(token); ok {
		t.Fatal("a session survived keep=\"\"")
	}
}

// A deleted account cannot keep browsing on a live cookie.
func TestDeletedAccountEndsSessions(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("temp", "pw", false); err != nil {
		t.Fatal(err)
	}
	token, _, _ := db.NewSession("temp", "pw")
	if err := db.DeleteAccount("temp"); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.SessionAccount(token); ok {
		t.Fatal("deleted account still resolves its session")
	}
}

// RFC 0008: a role is a name for a string. Granting one writes the
// preset, reading it back finds the preset, and a hand-edited string
// reads as the nearest role or as none at all.
func TestGrantWritesThePresetString(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	acct, _ := db.AccountByName("dan")
	if err := db.Grant("dan", "quarry", Maintain); err != nil {
		t.Fatal(err)
	}
	if caps, _ := db.CapsOf("dan", "quarry"); caps != Maintain.Caps() {
		t.Fatalf("grant wrote %q, want the Maintainer string %q", caps, Maintain.Caps())
	}
	if got := db.RoleOf(acct, "quarry"); got != Maintain {
		t.Fatalf("read back as %v", got)
	}
	if err := db.SetCaps("dan", "quarry", "uv3bBC7l"); err != nil { // Developer plus one
		t.Fatal(err)
	}
	if got := db.RoleOf(acct, "quarry"); got != Write {
		t.Fatalf("developer-plus-one read as %v", got)
	}
	if err := db.SetCaps("dan", "quarry", "iy"); err != nil { // a bot: no role names it
		t.Fatal(err)
	}
	if got := db.RoleOf(acct, "quarry"); got != NoRole {
		t.Fatalf("custom string read as %v", got)
	}
	if err := db.Revoke("dan", "quarry"); err != nil {
		t.Fatal(err)
	}
	if got := db.RoleOf(acct, "quarry"); got != NoRole {
		t.Fatalf("revoked but read as %v", got)
	}
}

// MayAt is the only gate: signed in meets the token floor whatever the
// string, and the admin floor is the a letter — a Maintainer does not
// have it, an account holding s does.
func TestMayAtGatesOnLetters(t *testing.T) {
	db := testDB(t)
	for _, n := range []string{"m", "s", "none"} {
		if err := db.AddAccount(n, "pw12345678", false); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.Grant("m", "quarry", Maintain); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("s", "quarry", "s"); err != nil {
		t.Fatal(err)
	}
	m, _ := db.AccountByName("m")
	s, _ := db.AccountByName("s")
	none, _ := db.AccountByName("none")
	if !db.MayAt(none, "quarry", false) || db.MayAt(none, "quarry", true) {
		t.Fatal("no string: token yes, admin no")
	}
	if !db.MayAt(m, "quarry", false) || db.MayAt(m, "quarry", true) {
		t.Fatal("maintainer: token yes, admin no")
	}
	if !db.MayAt(s, "quarry", true) {
		t.Fatal("s implies a")
	}
}

// Audit follow-up (ticket ed5bdd89c39a): logins are a closed alphabet.
func TestAccountNamesAreAClosedAlphabet(t *testing.T) {
	db := testDB(t)
	for _, ok := range []string{"dan", "ai-local", "a.b_c-9", "9lives"} {
		if err := db.AddAccount(ok, "pw12345678", false); err != nil {
			t.Fatalf("%q refused: %v", ok, err)
		}
	}
	for _, bad := range []string{"a b", "a'b", `a"b`, "<x>", "-lead", ".lead", "naïve", "a/b"} {
		if err := db.AddAccount(bad, "pw12345678", false); err == nil {
			t.Fatalf("%q accepted", bad)
		}
	}
}