Skip to content
quarry Artifact [11ddf83be5]
Login

Artifact [11ddf83be5]

Artifact 11ddf83be51ca1ce609cd988aa6d4d626ffccf6bddeeaf1a8e9e8d9e505874d3:


package oltp

import (
	"path/filepath"
	"testing"
)

const cutoverPW = "a-long-enough-password"

// cutoverRig is a store as the cutover finds it: a person, and agents
// that still hold a password from before marking cleared one — a
// person's agent with a native verifier, a forge agent, and a person's
// agent with only an imported fossil credential. The operator columns
// are written by hand, the way a pre-rung-3 mark left them.
func cutoverRig(t *testing.T, path string) *DB {
	t.Helper()
	db, err := Open(path)
	if err != nil {
		t.Fatal(err)
	}
	db.SyncProjectCode = func(string) (string, bool) { return "code", true }
	for _, who := range []string{"dan", "dan-bot", "zeit"} {
		if err := db.AddAccount(who, cutoverPW, false); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddRepo("pub", t.TempDir(), "", Public); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ImportUsers("pub", "code", []ImportedUser{
		{Login: "old-bot", Caps: "oi", PWHash: legacySecret("code", "old-bot", cutoverPW)},
	}); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"dan", "dan-bot", "zeit"} {
		if err := db.SetCaps(who, "pub", "oi"); err != nil {
			t.Fatal(err)
		}
		if err := db.SetSyncUser("pub", who, legacySecret("code", who, cutoverPW)); err != nil {
			t.Fatal(err)
		}
	}
	return db
}

func markByHand(t *testing.T, db *DB, name, operator string) {
	t.Helper()
	if _, err := db.w.Exec(`UPDATE account SET operator = ? WHERE name = ?`, operator, name); err != nil {
		t.Fatal(err)
	}
}

// Every password door is Authenticate or Secrets: an agent's password
// is refused at both even while one is still stored, and its tokens
// are not. A person's password is untouched.
func TestAgentPasswordRefusedAtEveryDoor(t *testing.T) {
	db := cutoverRig(t, filepath.Join(t.TempDir(), "oltp.db"))
	defer db.Close()
	reset, err := db.ResetPassword("dan-bot")
	if err != nil {
		t.Fatal(err)
	}
	markByHand(t, db, "dan-bot", "dan")
	markByHand(t, db, "old-bot", "dan")
	markByHand(t, db, "zeit", forgeOperator)

	for _, who := range []string{"dan-bot", "old-bot", "zeit"} {
		if _, ok := db.Authenticate(who, cutoverPW); ok {
			t.Errorf("FAIL: %s signed in with a stored password", who)
		}
		if _, _, ok := db.NewSessionFrom(who, cutoverPW, "", ""); ok {
			t.Errorf("FAIL: %s opened a web session with a password", who)
		}
		if err := db.SetPasswordJournalled(who, cutoverPW, "host", "account passwd"); !IsRefusal(err) {
			t.Errorf("FAIL: the host recovery gave %s a password: %v", who, err)
		}
	}
	if _, _, ok := db.LegacyCredential("old-bot"); !ok {
		t.Error("FAIL: the legacy credential was spent by a refused sign-in")
	}
	if _, err := db.RedeemInvite(reset, cutoverPW); err == nil {
		t.Error("FAIL: a reset minted before marking set the agent's password")
	}
	if _, ok := db.Authenticate("dan", cutoverPW); !ok {
		t.Error("FAIL: a person's password was refused")
	}

	// The login card: the hand-issued row is a password's secret and
	// goes unanswered for an agent; its sync token signs.
	if creds := db.Secrets("pub", "dan-bot"); len(creds) != 0 {
		t.Errorf("FAIL: an agent's hand-issued row still signs: %d credentials", len(creds))
	}
	if _, _, err := db.MintToken("dan-bot", TokenSpec{Name: "push", Scopes: []string{ScopeSync}}); err != nil {
		t.Fatal(err)
	}
	creds := db.Secrets("pub", "dan-bot")
	if len(creds) != 1 || creds[0].Token == "" {
		t.Errorf("FAIL: an agent's login card credentials = %+v, want its one sync token", creds)
	}
	if creds := db.Secrets("pub", "dan"); len(creds) != 1 || creds[0].Token != "" {
		t.Errorf("FAIL: a person's hand-issued row = %+v, want it answered", creds)
	}
	if !t.Failed() {
		t.Log("ok: password refused for both kinds of agent at sign-in, session, recovery, reset and login card; the person's still works")
	}
}

// The cutover migration clears every agent's stored password, native
// and imported, and the sessions it opened; a token-bound session and
// the tokens themselves survive, and so does the person.
func TestCutoverRetiresAgentPasswords(t *testing.T) {
	path := filepath.Join(t.TempDir(), "oltp.db")
	db := cutoverRig(t, path)
	byPassword, _, ok := db.NewSession("dan-bot", cutoverPW)
	if !ok {
		t.Fatal("no session before the mark")
	}
	markByHand(t, db, "dan-bot", "dan")
	markByHand(t, db, "old-bot", "dan")
	markByHand(t, db, "zeit", forgeOperator)
	value, tok, err := db.MintToken("dan-bot", TokenSpec{Name: "web", Scopes: []string{ScopeAPI, ScopeWeb}})
	if err != nil {
		t.Fatal(err)
	}
	link, _, err := db.MintSigninLink("dan-bot", tok.ID, "")
	if err != nil {
		t.Fatal(err)
	}
	byToken, _, _, ok := db.RedeemSigninLink(link, "", "")
	if !ok {
		t.Fatal("the sign-in link did not open a session")
	}
	db.Close()

	db, err = Open(path)
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	var left int
	if err := db.db.QueryRow(`SELECT count(*) FROM account WHERE operator <> ''
		AND (pwhash <> '' OR legacy_pw <> '' OR legacy_code <> '')`).Scan(&left); err != nil {
		t.Fatal(err)
	}
	if left != 0 {
		t.Errorf("FAIL: %d agents still hold a password after the cutover", left)
	}
	if _, _, _, ok := db.SessionPrincipal(byPassword); ok {
		t.Error("FAIL: the agent's password session outlived the cutover")
	}
	if _, _, bound, ok := db.SessionPrincipal(byToken); !ok || !bound {
		t.Error("FAIL: the agent's token-bound session was ended by the cutover")
	}
	if _, ok := db.ResolveToken("dan-bot", value, ScopeAPI); !ok {
		t.Error("FAIL: the agent's token stopped resolving")
	}
	var hash string
	if err := db.db.QueryRow(`SELECT pwhash FROM account WHERE name = 'dan'`).Scan(&hash); err != nil || hash == "" {
		t.Errorf("FAIL: the person's password went with the agents' (%v)", err)
	}
	if _, ok := db.Authenticate("dan", cutoverPW); !ok {
		t.Error("FAIL: a person's password was refused after the cutover")
	}
	if !t.Failed() {
		t.Log("ok: agents' passwords and password sessions retired; token, token session and person intact")
	}
}