Skip to content
quarry Artifact [b0dc5bc93f]
Login

Artifact [b0dc5bc93f]

Artifact b0dc5bc93fa1b035e891a7c52337b834680652f0b54d839820f49b2c933e7dc9:


package oltp

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

// A sync applies the categories and keeps everyone else as a list: no
// account is created, and a matching name is only suggested.
func TestFossilUsersAreAReferenceNotAccounts(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("bob", "a long password", false); err != nil {
		t.Fatal(err)
	}
	rows, cats, err := db.ReplaceFossilUsers("alpha", []FossilUser{
		{Login: "developer", Caps: "ei"},
		{Login: "alice", Caps: "ei", Info: "Alice Dev"},
		{Login: "bob", Caps: "k"},
	})
	if err != nil {
		t.Fatal(err)
	}
	if rows != 2 || cats != 1 {
		t.Fatalf("rows %d, categories %d", rows, cats)
	}
	if got := db.CategoryCaps("alpha", "developer"); got != "ei" {
		t.Fatalf("developer category = %q", got)
	}
	if _, ok := db.AccountByName("alice"); ok {
		t.Fatal("a sync created an account")
	}
	users, err := db.FossilUsers("alpha")
	if err != nil {
		t.Fatal(err)
	}
	if len(users) != 2 || users[0].Login != "alice" || users[0].Suggest || !users[1].Suggest {
		t.Fatalf("users = %+v", users)
	}
	if db.EffectiveCaps("bob", "alpha").HasAny("k") {
		t.Fatal("a matching name was granted without a link")
	}
}

// Only a link grants; a re-sync refreshes a linked account; a login
// that leaves fossil loses its row but not the account's access.
func TestFossilLinkGrantsAndSurvivesResync(t *testing.T) {
	db := testDB(t)
	if _, _, err := db.ReplaceFossilUsers("alpha", []FossilUser{{Login: "alice", Caps: "i"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "alpha", "alice", "al"); !errors.Is(err, ErrNoAccount) {
		t.Fatalf("link to a missing account: %v", err)
	}
	if err := linkErr(db, "alpha", "nobody-here", ""); !errors.Is(err, ErrNoFossilUser) {
		t.Fatalf("link of a missing login: %v", err)
	}
	if err := db.AddAccount("al", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "alpha", "alice", "al"); err != nil {
		t.Fatal(err)
	}
	if !db.EffectiveCaps("al", "alpha").HasAny("i") {
		t.Fatal("a link did not grant the login's capabilities")
	}
	if _, _, err := db.ReplaceFossilUsers("alpha", []FossilUser{{Login: "alice", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	users, _ := db.FossilUsers("alpha")
	if len(users) != 1 || users[0].Linked != "al" || !db.EffectiveCaps("al", "alpha").HasAny("k") {
		t.Fatalf("a re-sync lost the link or the refresh: %+v", users)
	}
	if _, _, err := db.ReplaceFossilUsers("alpha", nil); err != nil {
		t.Fatal(err)
	}
	if users, _ := db.FossilUsers("alpha"); len(users) != 0 {
		t.Fatalf("a login gone from fossil kept its row: %+v", users)
	}
	if !db.EffectiveCaps("al", "alpha").HasAny("k") {
		t.Fatal("a sync revoked an account's access without an admin")
	}
}

// linkErr links as an actor the guard has nothing to say about here —
// none of these links touches an admin — for the tests about what a
// link does rather than who may make it.
func linkErr(db *DB, repo, login, account string) error {
	_, err := db.LinkFossilUser("root", repo, login, account)
	return err
}

// Fossil's setup and admin letters are admin here, and an owner's hand
// grant survives the next sync.
func TestFossilSetupLinksAsAdmin(t *testing.T) {
	for _, caps := range []string{"s", "a"} {
		if got := roleFromCaps(caps); got != RepoAdmin {
			t.Errorf("caps %q read as %v, want admin", caps, got)
		}
	}
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "s"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
		t.Fatal(err)
	}
	g, _ := db.GrantsFor("dan")
	if g["zit"] != RepoAdmin {
		t.Fatalf("a linked setup user holds %v", g["zit"])
	}
}

// A rich string with no a or s is still the link's string, applied
// verbatim; it reads as no preset, and that reading is not a missing grant.
func TestFossilRichCapsApplyOnLink(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("ds", "a long password", false); err != nil {
		t.Fatal(err)
	}
	const rich = "bcfklmnqrvwxyz234567ACD"
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "danshearer", Caps: rich}}); err != nil {
		t.Fatal(err)
	}
	if got, _ := db.CapsOf("ds", "zit"); got != "" {
		t.Fatalf("an unlinked row granted %q", got)
	}
	if err := linkErr(db, "zit", "danshearer", "ds"); err != nil {
		t.Fatal(err)
	}
	if got, _ := db.CapsOf("ds", "zit"); got != rich {
		t.Fatalf("link stored %q, want %q", got, rich)
	}
}

func TestResyncKeepsAHandGrant(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "zit", RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	if got, _ := db.CapsOf("dan", "zit"); got != RepoAdmin.Caps() {
		t.Fatalf("a sync downgraded a hand grant to %q", got)
	}
	// Untouched since the last sync, an account still follows upstream.
	if err := db.AddAccount("al", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "alice", Caps: "i"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "alice", "al"); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "alice", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	if got, _ := db.CapsOf("al", "zit"); got != "k" {
		t.Fatalf("an untouched link stopped following: %q", got)
	}
}

// A string no preset is near is custom, never "none"; only an empty
// string reads as none.
func TestRichCapsNeverReadAsNone(t *testing.T) {
	if got := roleFromCaps("bcfklmnqrvwxyz234567ACD"); got == NoRole || got.String() != "custom" {
		t.Fatalf("a rich string reads %q", got)
	}
	if got := roleFromCaps(""); got != NoRole || got.String() != "none" {
		t.Fatalf("an empty string reads %q", got)
	}
	db := testDB(t)
	if err := db.AddAccount("ds", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("ds", "zit", "bcfklmnqrvwxyz234567ACD"); err != nil {
		t.Fatal(err)
	}
	g, _ := db.GrantsFor("ds")
	acct, _ := db.AccountByName("ds")
	if g["zit"].String() != "custom" || db.RoleOf(acct, "zit").String() != "custom" {
		t.Fatalf("grants %v, role %v", g["zit"], db.RoleOf(acct, "zit"))
	}
}

// Setup is the forge account's: a repository string never carries s,
// whether it arrives by link, by hand, or from a row stored before.
func TestRepoSetupNeverPassesSiteAdmin(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "s"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
		t.Fatal(err)
	}
	if got, _ := db.CapsOf("dan", "zit"); got != "a" {
		t.Fatalf("link stored %q, want a", got)
	}
	if db.EffectiveCaps("dan", "zit").HasAny("s") {
		t.Fatal("a linked setup user passes a site-admin check")
	}
	if !db.EffectiveCaps("dan", "zit").HasAny("a") {
		t.Fatal("a linked setup user lost admin")
	}
	// A row stored before the rule: read stripped, then migrated.
	if _, err := db.w.Exec(`INSERT INTO repo_caps (account, repo, caps) VALUES ('dan', 'old', 'sg')`); err != nil {
		t.Fatal(err)
	}
	if db.EffectiveCaps("dan", "old").HasAny("s") {
		t.Fatal("a stored s passes a site-admin check")
	}
	if err := db.MigrateSetupCaps(); err != nil {
		t.Fatal(err)
	}
	var raw string
	if err := db.db.QueryRow(`SELECT caps FROM repo_caps WHERE account = 'dan' AND repo = 'old'`).Scan(&raw); err != nil || raw != "ga" {
		t.Fatalf("migrated to %q (%v)", raw, err)
	}
	// A site admin keeps s at forge scope, and on no repository (#318).
	if err := db.AddAccount("root2", "a long password", true); err != nil {
		t.Fatal(err)
	}
	if !db.EffectiveCaps("root2", "").HasAny("s") {
		t.Fatal("a site admin lost s at forge scope")
	}
	if db.EffectiveCaps("root2", "zit").HasAny("s") {
		t.Fatal("a site admin holds s on a repository")
	}
}

// An upstream downgrade cannot take a repository's last admin.
func TestSyncKeepsTheLastAdmin(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "a"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	if !db.EffectiveCaps("dan", "zit").HasAny("a") {
		t.Fatal("a sync removed the repository's last admin")
	}
}

func TestGrantCustomRefuses(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("dan", "zit", "iy"); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("dan", "zit", Custom); err == nil {
		t.Fatal("Grant(Custom) did not refuse")
	}
	if _, err := db.GrantGuarded("root", "dan", "zit", Custom); err == nil {
		t.Fatal("GrantGuarded(Custom) did not refuse")
	}
	if got, _ := db.CapsOf("dan", "zit"); got != "iy" {
		t.Fatalf("a refused grant changed the string to %q", got)
	}
}

func TestFossilUsersReportAPinnedLink(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
		t.Fatal(err)
	}
	if us, _ := db.FossilUsers("zit"); us[0].Pinned {
		t.Fatal("a fresh link reads as pinned")
	}
	if err := db.Grant("dan", "zit", RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
		t.Fatal(err)
	}
	us, _ := db.FossilUsers("zit")
	if !us[0].Pinned || us[0].Held != RepoAdmin.Caps() || us[0].Caps != "k" {
		t.Fatalf("%+v", us[0])
	}
}

// Rows seeded raw, past every writer, as an older build or a hand edit
// would leave them: no read path may turn a stored s into setup.
func TestStoredSetupNeverReadsAsSetup(t *testing.T) {
	db := testDB(t)
	if err := db.AddAccount("dan", "a long password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.AddAccount("tm", "a long password", false); err != nil {
		t.Fatal(err)
	}
	raw := func(q string, args ...any) {
		t.Helper()
		if _, err := db.w.Exec(q, args...); err != nil {
			t.Fatal(err)
		}
	}
	raw(`INSERT INTO repo_caps (account, repo, caps) VALUES ('dan', 'r', 'su')`)
	if db.EffectiveCaps("dan", "r").HasAny("s") {
		t.Fatal("a stored repository s reads as setup")
	}
	if got, _ := db.CapsOf("dan", "r"); got != "ua" {
		t.Fatalf("CapsOf read %q", got)
	}
	hs, err := db.CapHolders("r")
	if err != nil {
		t.Fatal(err)
	}
	for _, h := range hs {
		if h.Account == "dan" && h.Caps != "ua" {
			t.Fatalf("CapHolders read %q", h.Caps)
		}
	}
	for _, cat := range []string{"nobody", "anonymous", "reader", "developer"} {
		raw(`INSERT OR REPLACE INTO repo_category_caps (repo, category, caps) VALUES ('c', ?, 'sa')`, cat)
	}
	raw(`INSERT INTO repo_caps (account, repo, caps) VALUES ('dan', 'c', 'uv')`)
	if db.EffectiveCaps("dan", "c").HasAny("s") || db.EffectiveCaps("dan", "c").HasAny("a") {
		t.Fatal("a stored category s reaches a signed-in account")
	}
	if db.EffectiveCaps("", "c").HasAny("s") || db.EffectiveCaps("", "c").HasAny("a") {
		t.Fatal("a stored category s reaches a signed-out visitor")
	}
	if got := db.CategoryCaps("c", "nobody"); got != "" {
		t.Fatalf("CategoryCaps read %q", got)
	}
	raw(`INSERT INTO team (name, title, created) VALUES ('t', 't', 'x')`)
	raw(`INSERT INTO team_member (team, account, caps) VALUES ('t', 'tm', 's')`)
	raw(`INSERT INTO team_repo (team, repo, added) VALUES ('t', 'tr', 'x')`)
	db.teamsExist.mu.Lock()
	db.teamsExist.checked = time.Time{} // the no-teams shortcut caches
	db.teamsExist.mu.Unlock()
	if db.EffectiveCaps("tm", "tr").HasAny("s") {
		t.Fatal("a stored team s reads as setup")
	}
}

// Every stored string is cleaned on open, and a fresh write cannot put
// s or a category admin back.
func TestOpenMigratesStoredSetup(t *testing.T) {
	path := filepath.Join(t.TempDir(), "oltp.db")
	db, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	for _, q := range []string{
		`INSERT INTO account (name, pwhash, admin, created) VALUES ('dan', '', 0, 'x')`,
		`INSERT INTO repo_caps (account, repo, caps) VALUES ('dan', 'r', 'gs')`,
		`INSERT INTO repo_category_caps (repo, category, caps) VALUES ('r', 'reader', 'ksa')`,
	} {
		if _, err := db.w.Exec(q); err != nil {
			t.Fatal(err)
		}
	}
	db.Close()
	db, err = Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	var caps, cat string
	db.db.QueryRow(`SELECT caps FROM repo_caps WHERE account = 'dan'`).Scan(&caps)
	db.db.QueryRow(`SELECT caps FROM repo_category_caps WHERE repo = 'r' AND category = 'reader'`).Scan(&cat)
	if caps != "ga" || cat != "k" {
		t.Fatalf("after open: repo %q, category %q", caps, cat)
	}
	if err := db.SetCategoryCaps("r", "anonymous", "sa"); err != nil {
		t.Fatal(err)
	}
	if got := db.CategoryCaps("r", "anonymous"); got != "" {
		t.Fatalf("a category write stored %q", got)
	}
}