Skip to content
quarry Artifact [08c64eb910]
Login

Artifact [08c64eb910]

Artifact 08c64eb910c67f770e399e78770202bb34e7872ac6c14030f9129ef63ff8d478:


package api

import (
	"bytes"
	"context"
	"encoding/json"
	"log"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/ops"
)

// resetToken is what resetAccounts mints, so a test can look for it in
// every place it must not be.
var resetToken = strings.Repeat("ab", 32)

// resetAccounts is fakeAccounts with the invite/reset half of
// AccountAdmin, counting the tokens it mints.
type resetAccounts struct {
	*fakeAccounts
	minted int
}

func (f *resetAccounts) InviteAccount(string, string, bool) (string, error) { return "", nil }
func (f *resetAccounts) InviteFor(string) (string, bool)                    { return "", false }
func (f *resetAccounts) RedeemInvite(string, string) (string, error)        { return "", nil }
func (f *resetAccounts) ResetPassword(string) (string, error) {
	f.minted++
	return resetToken, nil
}
func (f *resetAccounts) TokenKind(string) string                { return "reset" }
func (f *resetAccounts) SetDisabled(string, bool) error         { return nil }
func (f *resetAccounts) Sessions(string) ([]SessionInfo, error) { return nil, nil }
func (f *resetAccounts) EndSession(string, string) error        { return nil }
func (f *resetAccounts) EndSessions(string) error               { return nil }

// textMail records the plain part too: who asked and when is data the
// owner is owed, so the test reads it.
type textMail struct{ sent []struct{ To, Text string } }

func (m *textMail) Send(_ context.Context, to, _, text, _ string) (string, error) {
	m.sent = append(m.sent, struct{ To, Text string }{to, text})
	return "id", nil
}

func resetForge(t *testing.T) (*ops.Registry, *resetAccounts, *textMail, *recordedActs) {
	t.Helper()
	accts := &resetAccounts{fakeAccounts: newFakeAccounts()}
	_ = accts.AddAccount("root", "root-password", true)
	_ = accts.AddAccount("jo", "jos-password", false)
	_ = accts.SetEmail("jo", "[email protected]")
	_ = accts.AddAccount("nomail", "nomails-password", false)
	mail, acts := &textMail{}, &recordedActs{}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail, Acts: acts,
		Origin: "https://forge.example"}); err != nil {
		t.Fatal(err)
	}
	return r, accts, mail, acts
}

// No door lets a site admin choose somebody else's password: the op
// that did is gone, so there is nothing to call.
func TestAnAdminCannotSetAnotherAccountsPassword(t *testing.T) {
	r, accts, _, _ := resetForge(t)
	if _, ok := r.Lookup("account.passwd"); ok {
		t.Fatal("FAIL: account.passwd is still registered")
	}
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.passwd", map[string]any{"name": "jo", "password": "chosen-by-root"}); err == nil {
		t.Fatal("FAIL: account.passwd answered")
	}
	if accts.pw["jo"] != "jos-password" {
		t.Fatal("FAIL: jo's password changed")
	}
	t.Log("ok: no op sets another account's password")
}

// The one thing an admin may do: ask for a link to go to the owner's
// own confirmed address. One email, naming the admin and the time; the
// token is in no response, no log line and no receipt.
func TestAnAdminResetMailsTheOwnerAndHandsBackNoToken(t *testing.T) {
	r, accts, mail, acts := resetForge(t)
	var logged bytes.Buffer
	prev := log.Writer()
	log.SetOutput(&logged)
	defer log.SetOutput(prev)

	before := time.Now().UTC()
	ctx := ops.WithUser(context.Background(), "root")
	out, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "jo"})
	if err != nil {
		t.Fatal(err)
	}
	after := time.Now().UTC()

	if len(mail.sent) != 1 || mail.sent[0].To != "[email protected]" {
		t.Fatalf("FAIL: want one mail to [email protected], got %+v", mail.sent)
	}
	text := mail.sent[0].Text
	if !strings.Contains(text, "root") {
		t.Error("FAIL: the email does not name the admin who asked")
	}
	if !strings.Contains(text, before.Format(resetStamp)) && !strings.Contains(text, after.Format(resetStamp)) {
		t.Error("FAIL: the email does not say when it was asked for")
	}
	// The link is built on the forge's own origin, never one the caller
	// passed: a link to somewhere else would hand the token to whoever
	// runs that somewhere else.
	if !strings.Contains(text, "https://forge.example/invite/"+resetToken) {
		t.Error("FAIL: the link is not on the forge's own origin")
	}

	wire, _ := json.Marshal(out)
	if strings.Contains(string(wire), resetToken) || strings.Contains(string(wire), "/invite/") {
		t.Errorf("FAIL: the response carries the token: %s", wire)
	}
	if strings.Contains(logged.String(), resetToken) {
		t.Error("FAIL: the token reached the log")
	}

	if len(acts.acts) != 1 {
		t.Fatalf("FAIL: want one receipt, got %+v", acts.acts)
	}
	got := acts.acts[0]
	if got.Actor != "root" || got.Op != "account.reset" || got.Owner != "jo" {
		t.Errorf("FAIL: receipt = %+v", got)
	}
	if strings.Contains(got.Actor+got.Op+got.Repo+got.Owner, resetToken) {
		t.Error("FAIL: the token is in the receipt")
	}
	if accts.minted != 1 {
		t.Errorf("FAIL: minted %d tokens", accts.minted)
	}
	t.Log("ok: one mail to the owner, naming root and the time; no token handed back, logged or receipted")
}

// An account with no confirmed address cannot be reset from the forge
// at all — and nothing is minted for it, since a token nobody was sent
// is a token waiting to leak. A pending address is not confirmed.
func TestAnAdminResetRefusesAnAccountWithNoConfirmedAddress(t *testing.T) {
	r, accts, mail, acts := resetForge(t)
	_, _ = accts.SetPendingEmail("nomail", "[email protected]")
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "nomail"}); err == nil {
		t.Fatal("FAIL: a reset was accepted for an account with no confirmed address")
	}
	if accts.minted != 0 || len(mail.sent) != 0 || len(acts.acts) != 0 {
		t.Fatalf("FAIL: refused reset still minted=%d mailed=%d receipted=%d", accts.minted, len(mail.sent), len(acts.acts))
	}
	t.Log("ok: refused, nothing minted, nothing sent")
}

// With no mail plane the link has nowhere to go but back to the admin,
// which is the thing this op no longer does.
func TestAnAdminResetRefusesWithNoMail(t *testing.T) {
	accts := &resetAccounts{fakeAccounts: newFakeAccounts()}
	_ = accts.AddAccount("root", "root-password", true)
	_ = accts.AddAccount("jo", "jos-password", false)
	_ = accts.SetEmail("jo", "[email protected]")
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Origin: "https://forge.example"}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "jo"}); err == nil {
		t.Fatal("FAIL: a reset was accepted with no way to send it")
	}
	if accts.minted != 0 {
		t.Fatal("FAIL: a token was minted that nobody could be sent")
	}
}

// An admin is still an account: their OWN password changes the way
// everybody's does, given the current one.
func TestAnAdminStillChangesTheirOwnPassword(t *testing.T) {
	r, accts, _, _ := resetForge(t)
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "me.passwd", mePasswdIn{Current: "root-password", New: "a-new-root-password"}); err != nil {
		t.Fatal(err)
	}
	if accts.pw["root"] != "a-new-root-password" {
		t.Fatal("FAIL: root's own password did not change")
	}
	t.Log("ok: an admin changes their own password through me.passwd")
}

// The bypass #319 closes: an admin points the account's address at
// their own inbox, then asks for a reset. The address stays pending, so
// the reset goes to the OLD confirmed address — and the old address was
// told who did it and when.
func TestAnAdminCannotRedirectAResetToTheirOwnInbox(t *testing.T) {
	r, accts, mail, acts := resetForge(t)
	ctx := ops.WithUser(context.Background(), "root")
	before := time.Now().UTC()
	if _, err := r.Invoke(ctx, "account.email.set", accountEmailSetIn{Name: "jo", Email: "[email protected]"}); err != nil {
		t.Fatal(err)
	}
	after := time.Now().UTC()
	if a, _ := accts.AccountByName("jo"); a.Email != "[email protected]" || a.EmailPending != "[email protected]" {
		t.Fatalf("FAIL: account state after admin set = %+v", a)
	}
	var told bool
	for _, m := range mail.sent {
		if m.To == "[email protected]" && strings.Contains(m.Text, "root") &&
			(strings.Contains(m.Text, before.Format(resetStamp)) || strings.Contains(m.Text, after.Format(resetStamp))) {
			told = true
		}
	}
	if !told {
		t.Errorf("FAIL: the confirmed address was not told who and when: %+v", mail.sent)
	}
	if len(acts.acts) != 1 || acts.acts[0].Actor != "root" || acts.acts[0].Op != "account.email.set" || acts.acts[0].Owner != "jo" {
		t.Errorf("FAIL: receipt = %+v", acts.acts)
	}

	mail.sent = nil
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "jo"}); err != nil {
		t.Fatal(err)
	}
	if len(mail.sent) != 1 || mail.sent[0].To != "[email protected]" {
		t.Fatalf("FAIL: the reset went to %+v, not jo's confirmed address", mail.sent)
	}
	t.Log("ok: the reset still goes to jo's confirmed address")
}

// With no confirmed address to fall back on, the pending one an admin
// set does not make the account resettable.
func TestAPendingAddressDoesNotMakeAnAccountResettable(t *testing.T) {
	r, accts, mail, _ := resetForge(t)
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.email.set", accountEmailSetIn{Name: "nomail", Email: "[email protected]"}); err != nil {
		t.Fatal(err)
	}
	mail.sent = nil
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "nomail"}); err == nil {
		t.Fatal("FAIL: a reset was accepted on an admin-set pending address")
	}
	if accts.minted != 0 || len(mail.sent) != 0 {
		t.Fatalf("FAIL: minted=%d mailed=%+v", accts.minted, mail.sent)
	}
	t.Log("ok: refused")
}

// The code reaches whoever holds the new inbox — here, the admin. It
// still confirms nothing except from the owner's own session.
func TestTheCodeConfirmsOnlyFromTheOwnersSession(t *testing.T) {
	r, accts, _, _ := resetForge(t)
	rootCtx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(rootCtx, "account.email.set", accountEmailSetIn{Name: "jo", Email: "[email protected]"}); err != nil {
		t.Fatal(err)
	}
	code := accts.pending["jo"]
	for _, ctx := range []context.Context{rootCtx, context.Background()} {
		if _, err := r.Invoke(ctx, "me.email.confirm", meEmailConfirmIn{Code: code}); err == nil {
			t.Errorf("FAIL: the code confirmed from a session that is not jo's (%q)", ops.User(ctx))
		}
	}
	if a, _ := accts.AccountByName("jo"); a.Email != "[email protected]" {
		t.Fatalf("FAIL: jo's confirmed address moved to %q", a.Email)
	}
	if a, _ := accts.AccountByName("root"); a.Email == "[email protected]" {
		t.Fatal("FAIL: the code confirmed an address on root's own account")
	}
	if _, err := r.Invoke(ops.WithUser(context.Background(), "jo"), "me.email.confirm", meEmailConfirmIn{Code: code}); err != nil {
		t.Fatalf("FAIL: jo, signed in, could not confirm: %v", err)
	}
	t.Log("ok: only jo's own session confirms")
}