Skip to content
quarry Artifact [ed5f21ea91]
Login

Artifact [ed5f21ea91]

Artifact ed5f21ea91dabd82c6981a843747c7f6b120d08d28c55e2b2146e7e2fac17dcd:


package oltp

import (
	"strings"
	"testing"
)

// The expansion rules are fossil's, kept exactly (RFC 0004 §3): every
// visitor holds the nobody set; logging in adds anonymous; u inherits
// the reader category, v the developer category — and v does NOT imply
// u; a grants everything except s, x, y; s grants everything.
func TestEffectiveCapsExpansion(t *testing.T) {
	db := testDB(t)
	// Anonymous visitor: stock nobody.
	eff := db.EffectiveCaps("", "r")
	for _, c := range []string{"g", "j", "o", "r", "z"} {
		if !eff.HasAny(c) {
			t.Fatalf("visitor lacks nobody cap %q (eff=%q)", c, eff)
		}
	}
	// RFC 0008's Public project gives visitors h and 2 on top of stock;
	// i stays behind a sign-in.
	if !eff.HasAny("h") || !eff.HasAny("2") || eff.HasAny("i") {
		t.Fatalf("visitor set is not Public project's: %q", eff)
	}

	// A signed-in account with no explicit caps: nobody + anonymous.
	if err := db.AddAccount("plain", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	eff = db.EffectiveCaps("plain", "r")
	for _, c := range []string{"h", "m", "n", "c", "o"} {
		if !eff.HasAny(c) {
			t.Fatalf("signed-in lacks %q (eff=%q)", c, eff)
		}
	}
	if eff.HasAny("k") || eff.HasAny("i") {
		t.Fatalf("signed-in holds category caps unasked: %q", eff)
	}

	// u inherits the reader category (stock kptw).
	if err := db.SetCaps("plain", "r", "u"); err != nil {
		t.Fatal(err)
	}
	eff = db.EffectiveCaps("plain", "r")
	if !eff.HasAny("k") || !eff.HasAny("w") {
		t.Fatalf("u did not expand to reader caps: %q", eff)
	}
	if eff.HasAny("i") {
		t.Fatalf("u granted developer caps: %q", eff)
	}

	// v inherits developer (stock ei) — and NOT reader. Fossil's trap,
	// kept deliberately.
	if err := db.SetCaps("plain", "r", "v"); err != nil {
		t.Fatal(err)
	}
	eff = db.EffectiveCaps("plain", "r")
	if !eff.HasAny("i") || !eff.HasAny("e") {
		t.Fatalf("v did not expand to developer caps: %q", eff)
	}
	if eff.HasAny("k") || eff.HasAny("w") {
		t.Fatalf("v implied u — fossil's rule broken: %q", eff)
	}
}

func TestAdminAndSetupRules(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("boss", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("boss", "r", "a"); err != nil {
		t.Fatal(err)
	}
	eff := db.EffectiveCaps("boss", "r")
	for _, c := range []string{"i", "k", "w", "5", "e"} {
		if !eff.HasAny(c) {
			t.Fatalf("a does not grant %q", c)
		}
	}
	for _, c := range []string{"s", "x", "y"} {
		if eff.HasAny(c) {
			t.Fatalf("a granted excluded cap %q", c)
		}
	}
	// s grants everything, including the exclusions.
	if err := db.SetCaps("boss", "r", "s"); err != nil {
		t.Fatal(err)
	}
	eff = db.EffectiveCaps("boss", "r")
	for _, c := range []string{"a", "s", "x", "y", "i", "5"} {
		if !eff.HasAny(c) {
			t.Fatalf("s does not grant %q", c)
		}
	}
	// The site-admin account flag is s at forge scope, and only there:
	// at a repository a site admin holds what their grants say (#318).
	if err := db.AddAccount("root", "pw12345678", true); err != nil {
		t.Fatal(err)
	}
	if !db.EffectiveCaps("root", "").HasAny("s") {
		t.Fatal("site admin lacks s at forge scope")
	}
	if eff := db.EffectiveCaps("root", "anything"); strings.ContainsRune(string(eff), 's') || eff.HasAny("a") {
		t.Fatalf("site admin holds %q on a repository nobody granted", eff)
	}
}

func TestPerRepoCategoryOverride(t *testing.T) {
	db := testDB(t)
	// A repo "taken private": nobody and anonymous blanked (fossil's
	// Security-Audit action). Visitors hold nothing.
	if err := db.SetCategoryCaps("priv", "nobody", ""); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCategoryCaps("priv", "anonymous", ""); err != nil {
		t.Fatal(err)
	}
	if eff := db.EffectiveCaps("", "priv"); eff.HasAny("gjorz2h") {
		t.Fatalf("private repo leaks visitor caps: %q", eff)
	}
	// Another repo on the same forge keeps stock defaults.
	if eff := db.EffectiveCaps("", "open"); !eff.HasAny("o") {
		t.Fatalf("open repo lost stock nobody: %q", eff)
	}
}

// A forge that predates RFC 0008 opens with a repo_role table. Its rows
// become the strings they meant at the time — not the wider presets —
// explicit strings win, and the table is gone afterwards so the next
// open has nothing to do.
func TestRoleRowsTranslateToCaps(t *testing.T) {
	db := testDB(t)
	must := func(err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	for _, n := range []string{"r1", "w1", "a1", "x1"} {
		must(db.AddAccount(n, "pw12345678", false))
	}
	_, err := db.db.Exec(`CREATE TABLE repo_role (account TEXT, repo TEXT, role TEXT, PRIMARY KEY (account, repo));
		INSERT INTO repo_role VALUES ('r1','repo','read'), ('w1','repo','write'), ('a1','repo','admin'), ('x1','repo','write')`)
	must(err)
	must(db.SetCaps("x1", "repo", "u"))
	must(db.MigrateRolesToCaps())
	for _, c := range []struct{ who, want string }{{"r1", "u"}, {"w1", "uv"}, {"a1", "a"}, {"x1", "u"}} {
		if got, _ := db.CapsOf(c.who, "repo"); got != c.want {
			t.Fatalf("%s: got %q want %q", c.who, got, c.want)
		}
	}
	var n int
	must(db.db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE name = 'repo_role'`).Scan(&n))
	if n != 0 {
		t.Fatal("repo_role survived the migration")
	}
	must(db.MigrateRolesToCaps()) // nothing to do is not an error
}

func TestPurgeRepoSweepsCaps(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("u1", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("u1", "gone", "uv"); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCategoryCaps("gone", "nobody", ""); err != nil {
		t.Fatal(err)
	}
	if err := db.PurgeRepo("gone"); err != nil {
		t.Fatal(err)
	}
	if caps, _ := db.CapsOf("u1", "gone"); caps != "" {
		t.Fatalf("caps survived purge: %q", caps)
	}
	if eff := db.EffectiveCaps("", "gone"); !eff.HasAny("o") {
		t.Fatal("category override survived purge (stock should return)")
	}
}

// Importing a peer's users creates unknown logins with their fossil
// credential, replaces caps on THIS repo, and never takes over an
// account quarry already has.
func TestImportUsers(t *testing.T) {
	db := testDB(t)
	// A login quarry already knows, with its own password.
	if err := db.AddAccount("alice", "quarrypw123", false); err != nil {
		t.Fatal(err)
	}
	before, _ := db.AccountByName("alice")
	_ = before

	created, updated, err := db.ImportUsers("repo", "code123", []ImportedUser{
		{Login: "alice", Caps: "ei", PWHash: "peerhashalice"},
		{Login: "bob", Caps: "kptw", PWHash: "peerhashbob", Info: "Bob"},
		{Login: "nobody", Caps: "gjor"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if created != 1 {
		t.Fatalf("created = %d, want 1 (bob)", created)
	}
	if updated != 2 {
		t.Fatalf("updated = %d, want 2 (alice, nobody)", updated)
	}
	// Caps arrived verbatim for both.
	if c, _ := db.CapsOf("alice", "repo"); c != "ei" {
		t.Fatalf("alice's caps = %q", c)
	}
	if c, _ := db.CapsOf("bob", "repo"); c != "kptw" {
		t.Fatalf("bob's caps = %q", c)
	}
	// The category row landed as a category, not as an account.
	if db.CategoryCaps("repo", "nobody") != "gjor" {
		t.Fatalf("nobody category = %q", db.CategoryCaps("repo", "nobody"))
	}
	if _, found := db.AccountByName("nobody"); found {
		t.Fatal("a category became an account")
	}
	// alice keeps HER credential: an import is not a takeover.
	if !VerifyPassword(mustPWHash(t, db, "alice"), "quarrypw123") {
		t.Fatal("import overwrote an existing account's password")
	}
	if _, _, ok := db.LegacyCredential("alice"); ok {
		t.Fatal("an existing account gained an imported credential")
	}
	// bob can be verified against the peer's hash until he sets one.
	h, code, ok := db.LegacyCredential("bob")
	if !ok || h != "peerhashbob" || code != "code123" {
		t.Fatalf("bob's legacy credential = %q/%q ok=%v", h, code, ok)
	}
	if err := db.ClearLegacyCredential("bob"); err != nil {
		t.Fatal(err)
	}
	if _, _, ok := db.LegacyCredential("bob"); ok {
		t.Fatal("legacy credential survived being cleared")
	}
	// A category the peer never mentioned keeps quarry's stock default.
	if db.CategoryCaps("repo", "reader") != StockReader {
		t.Fatal("an unmentioned category was blanked")
	}
}

func mustPWHash(t *testing.T, db *DB, name string) string {
	t.Helper()
	var h string
	if err := db.db.QueryRow(`SELECT pwhash FROM account WHERE name = ?`, name).Scan(&h); err != nil {
		t.Fatal(err)
	}
	return h
}

// An imported fossil user signs in with the password they already had,
// once — and the account is upgraded to a native verifier on the way
// through, so the imported secret does not outlive its usefulness.
func TestImportedUserSignsInWithFossilPassword(t *testing.T) {
	db := testDB(t)
	const code, login, pw = "abc123projectcode", "alice", "secretpw"
	if _, _, err := db.ImportUsers("repo", code, []ImportedUser{
		{Login: login, Caps: "ei", PWHash: legacySecret(code, login, pw)},
	}); err != nil {
		t.Fatal(err)
	}
	// The fossil password works.
	if _, ok := db.Authenticate(login, pw); !ok {
		t.Fatal("an imported user could not sign in with their fossil password")
	}
	// It was an upgrade, not a permanent bypass: the imported secret is
	// gone and a native verifier now stands in its place.
	if _, _, ok := db.LegacyCredential(login); ok {
		t.Fatal("the imported credential survived a successful login")
	}
	if _, ok := db.Authenticate(login, pw); !ok {
		t.Fatal("the upgraded account cannot sign in with the same password")
	}
	if _, ok := db.Authenticate(login, "wrong"); ok {
		t.Fatal("a wrong password was accepted")
	}
}

// A wrong password never consumes the legacy credential — otherwise a
// failed guess would lock the real owner out of their own upgrade path.
func TestWrongPasswordLeavesTheLegacyCredential(t *testing.T) {
	db := testDB(t)
	const code, login, pw = "code", "bob", "hunter2"
	if _, _, err := db.ImportUsers("repo", code, []ImportedUser{
		{Login: login, PWHash: legacySecret(code, login, pw)},
	}); err != nil {
		t.Fatal(err)
	}
	if _, ok := db.Authenticate(login, "nope"); ok {
		t.Fatal("wrong password accepted")
	}
	if _, _, ok := db.LegacyCredential(login); !ok {
		t.Fatal("a failed guess destroyed the legacy credential")
	}
	if _, ok := db.Authenticate(login, pw); !ok {
		t.Fatal("the real password stopped working after a failed guess")
	}
}

// Audit follow-up (ticket ed5bdd89c39a): the fleet default is what a
// NEW REPOSITORY starts as, never what the repo-less caps resolution
// hands out — a visitor's caps for repo "" stay fossil stock, so
// seeding Public project does not open people.list to the signed-out.
func TestFleetDefaultDoesNotLeakIntoRepolessCaps(t *testing.T) {
	db := testDB(t)
	// The seeded default would grant h and 2; stock nobody does not.
	if eff := db.EffectiveCaps("", ""); eff.HasAny("h") || eff.HasAny("2") {
		t.Fatalf("signed-out repo-less caps carry the fleet default: %q", eff)
	}
	if eff := db.EffectiveCaps("", ""); !eff.HasAny("o") {
		t.Fatalf("stock nobody should still read: %q", eff)
	}
	// A real repository still takes the fleet default.
	if err := db.EnsureRepo("r", t.TempDir()); err != nil {
		t.Fatal(err)
	}
	if eff := db.EffectiveCaps("", "r"); !eff.HasAny("h") {
		t.Fatalf("a repository no longer takes the fleet default: %q", eff)
	}
}

// Audit follow-up (ticket ed5bdd89c39a): a repo_role row the migration
// does not recognise keeps the table alive — known rows still migrate,
// nothing is silently destroyed, and a later open retries.
func TestMigrationKeepsUnknownRoleRows(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("r1", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	if _, err := db.db.Exec(`CREATE TABLE repo_role (account TEXT, repo TEXT, role TEXT, PRIMARY KEY (account, repo));
		INSERT INTO repo_role VALUES ('r1','repo','read'), ('r1','other','wizard')`); err != nil {
		t.Fatal(err)
	}
	if err := db.MigrateRolesToCaps(); err != nil {
		t.Fatal(err)
	}
	if caps, _ := db.CapsOf("r1", "repo"); caps != "u" {
		t.Fatalf("known row not migrated: %q", caps)
	}
	var n int
	if err := db.db.QueryRow(`SELECT count(*) FROM sqlite_master WHERE name = 'repo_role'`).Scan(&n); err != nil || n != 1 {
		t.Fatalf("table dropped with an unknown row still in it (n=%d, %v)", n, err)
	}
}

// SourcesWith must describe exactly what effective() grants: every
// letter an account holds has a source, nothing outside that set is
// attributed, and each letter names the BROADEST grant — the one whose
// removal would actually take it away. Held together across the roles
// quarry ships, a site administrator, and both category letters.
func TestSourcesCoverExactlyTheEffectiveSet(t *testing.T) {
	db := testDB(t)
	for _, who := range []string{"dev", "rdr", "plain"} {
		if err := db.AddAccount(who, "pw12345678", false); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddAccount("root", "pw12345678", true); err != nil {
		t.Fatal(err)
	}
	for acct, caps := range map[string]string{"dev": "uv3bBC78", "rdr": "bcghjoprz2378BC"} {
		if err := db.SetCaps(acct, "r", caps); err != nil {
			t.Fatal(err)
		}
	}
	for _, acct := range []string{"", "plain", "dev", "rdr", "root"} {
		own, _ := db.CapsOf(acct, "r")
		eff := string(db.EffectiveCaps(acct, "r"))
		src := db.SourcesWith(acct, "r", own)
		for _, r := range eff {
			if _, ok := src[r]; !ok {
				t.Errorf("FAIL %q holds %c with no source", acct, r)
			}
		}
		for r := range src {
			if !strings.ContainsRune(eff, r) {
				t.Errorf("FAIL %q: %c attributed but not held", acct, r)
			}
		}
	}

	dev, _ := db.CapsOf("dev", "r")
	src := db.SourcesWith("dev", "r", dev)
	for letter, want := range map[rune]string{
		'i': SourceDeveloper, // check-in: the developer category, via v
		'k': SourceReader,    // edit wiki: the reader category, via u
		'o': SourceNobody,    // read the code: every visitor, not the role
		'n': SourceAnonymous, // file tickets: anyone signed in
	} {
		if got := src[letter]; got != want {
			t.Errorf("FAIL a Developer's %c comes from %q, want %q", letter, got, want)
		} else {
			t.Logf("ok %c from %s", letter, got)
		}
	}
	// "own" stated as a property rather than a guessed letter: every
	// letter in the Developer string that neither visitor category grants
	// is explained by the string itself. Two guesses at such a letter
	// were wrong, because this database's defaults give chat and CI to
	// anyone signed in — and the broadest grant is the right answer.
	broad := db.CategoryCaps("r", "nobody") + db.CategoryCaps("r", "anonymous")
	ownOnly := 0
	for _, r := range dev {
		if strings.ContainsRune(broad, r) {
			continue
		}
		ownOnly++
		if src[r] != SourceOwn {
			t.Errorf("FAIL %c is only in the Developer string but comes from %q", r, src[r])
		}
	}
	if ownOnly == 0 {
		t.Fatal("FAIL the fixture has no letter only the Developer string grants")
	}
	t.Logf("ok %d letters only the string grants are attributed to it", ownOnly)
	if got := db.SourcesWith("root", "", "")['s']; got != SourceSite {
		t.Errorf("FAIL a site administrator's s at forge scope comes from %q", got)
	}
	if got, ok := db.SourcesWith("root", "r", "")['s']; ok {
		t.Errorf("FAIL a site administrator's s on a repository is attributed to %q", got)
	}
}