Skip to content
quarry Artifact [a55712759f]
Login

Artifact [a55712759f]

Artifact a55712759f6915b45d50d525401dc28d1cdd80e22a1dd7394f1b97b40adb8647:


package web

import (
	"net/http"
	"net/url"
	"strings"
	"testing"
)

// A forgotten password used to have exactly one answer on this forge:
// an operator choosing a password for somebody and repeating it down a
// channel neither of them controls. This is the other one — the person
// chooses their own, through the same one-time link an invite already
// used, and nobody else ever holds it.
func TestAForgottenPasswordIsResetThroughALink(t *testing.T) {
	srv, accts, _ := betaForge(t)

	// Somebody with an account and a password they have forgotten.
	if err := accts.AddAccount("jo", "the-old-password", false); err != nil {
		t.Fatal(err)
	}
	res := as(t, srv, "root", "POST", "/fleet/people/reset", url.Values{"name": {"jo"}})
	body(res)
	if res.StatusCode != 303 {
		t.Fatalf("FAIL: reset answered %d", res.StatusCode)
	}
	// FOLLOW the redirect, query and all. The link no longer rides the
	// flash — the flash carries the outcome and the URL carries a
	// single-use nonce the vault swaps for the link (secretvault.go) —
	// so a test that rebuilds the path by hand drops the nonce and
	// reports the reveal broken when it is not.
	where := res.Header.Get("Location")
	if !strings.Contains(where, "?s=") {
		t.Fatalf("FAIL: the redirect carries no vault nonce: %q", where)
	}
	req, _ := http.NewRequest("GET", srv.URL+where, nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "tok-root"})
	for _, c := range res.Cookies() {
		req.AddCookie(c)
	}
	res2, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	page := body(res2)
	token := strings.Repeat("cd", 32)
	if !strings.Contains(page, "/invite/"+token) {
		t.Fatalf("FAIL: the People tab does not show the reset link")
	}
	t.Log("ok: the operator is handed a link")
	// It says which of the two things happened. An operator who reads
	// "Invited" here has been told the wrong thing about somebody who
	// already had an account.
	if !strings.Contains(page, "Reset.") {
		t.Error("FAIL: the banner does not say this was a reset")
	} else {
		t.Log("ok: the banner names it a reset")
	}

	// The old password still works: a reset that locked the account
	// would make a lost link worse than the problem it was sent about.
	if res := as(t, srv, "", "POST", "/login",
		url.Values{"user": {"jo"}, "password": {"the-old-password"}}); res.StatusCode != 303 {
		t.Errorf("FAIL: minting a reset locked jo out (login answered %d)", res.StatusCode)
	} else {
		t.Log("ok: the old password holds until the link is used")
	}

	// The link greets the right person with the right words.
	open := body(as(t, srv, "", "GET", "/invite/"+token, nil))
	if !strings.Contains(open, "Set a new password") || !strings.Contains(open, "jo") {
		t.Error("FAIL: the link does not offer jo a new password")
	} else {
		t.Log("ok: the link offers a new password, not a welcome")
	}

	// And it works.
	if res := as(t, srv, "", "POST", "/invite/"+token,
		url.Values{"password": {"a-brand-new-password"}, "again": {"a-brand-new-password"}}); res.StatusCode != 303 {
		t.Fatalf("FAIL: setting the new password answered %d", res.StatusCode)
	}
	if res := as(t, srv, "", "POST", "/login",
		url.Values{"user": {"jo"}, "password": {"a-brand-new-password"}}); res.StatusCode != 303 {
		t.Error("FAIL: the new password does not sign jo in")
	} else {
		t.Log("ok: jo signs in with the password jo chose")
	}
}

// Only a site admin may mint one. A reset link is a credential for
// somebody else's account, so the gate here is the gate on the page.
func TestOnlyAnAdminMayResetAPassword(t *testing.T) {
	srv, accts, _ := betaForge(t)
	if err := accts.AddAccount("jo", "the-old-password", false); err != nil {
		t.Fatal(err)
	}
	if err := accts.AddAccount("mallory", "mallorys-password", false); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"", "mallory"} {
		res := as(t, srv, who, "POST", "/fleet/people/reset", url.Values{"name": {"jo"}})
		body(res)
		// A 303 is not success here: signed out, the gate answers with
		// a redirect to the login page. Read WHERE it points, or a
		// refusal reads as a reset.
		where := res.Header.Get("Location")
		refused := res.StatusCode != 303 || strings.HasPrefix(where, "/login")
		if !refused {
			t.Errorf("FAIL: %q minted a reset for somebody else (%d -> %s)", who, res.StatusCode, where)
		} else {
			t.Logf("ok: %q was refused (%d -> %s)", who, res.StatusCode, where)
		}
	}
}