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