Skip to content
quarry Artifact [dd23598a56]
Login

Artifact [dd23598a56]

Artifact dd23598a56a4fae0a17466d29607767cd29037161578ba501153835ad3bc59fd:


package web

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

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

// linkSessions opens one session for one link, once.
type linkSessions struct {
	*betaSessions
	spent bool
}

func (l *linkSessions) RedeemSigninLink(link, ip, agent string) (string, time.Time, string, bool) {
	if link != "good" || l.spent {
		return "", time.Time{}, "", false
	}
	l.spent = true
	return "cookie-from-link", time.Now().Add(time.Hour), "/r/quarry", true
}

func signinServer(t *testing.T, sess Sessions) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v", Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{WithSessions(sess)},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })))
	t.Cleanup(srv.Close)
	return srv
}

func getNoFollow(t *testing.T, url string) *http.Response {
	t.Helper()
	client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	res, err := client.Get(url)
	if err != nil {
		t.Fatal(err)
	}
	return res
}

// A SIGN-IN LINK OPENS ONE SESSION AND REDIRECTS AT ONCE WITH NO
// REFERRER; THE SECOND GET, AND ANY UNKNOWN LINK, IS ONE PAGE WITH NO
// REDIRECT.
func TestSigninLinkRedirectsOnceAndSpends(t *testing.T) {
	accts := newBetaAccts()
	srv := signinServer(t, &linkSessions{betaSessions: &betaSessions{accts: accts}})

	res := getNoFollow(t, srv.URL+"/signin/good")
	var cookie *http.Cookie
	for _, c := range res.Cookies() {
		if c.Name == sessionCookie {
			cookie = c
		}
	}
	if res.StatusCode != http.StatusSeeOther || res.Header.Get("Location") != "/r/quarry" {
		t.Errorf("FAIL: first GET = %d to %q; want 303 to /r/quarry", res.StatusCode, res.Header.Get("Location"))
	}
	if res.Header.Get("Referrer-Policy") != "no-referrer" {
		t.Errorf("FAIL: Referrer-Policy = %q; want no-referrer", res.Header.Get("Referrer-Policy"))
	}
	if cookie == nil || cookie.Value != "cookie-from-link" || !cookie.HttpOnly {
		t.Errorf("FAIL: session cookie = %+v; want the link's, HttpOnly", cookie)
	}
	for _, path := range []string{"/signin/good", "/signin/unknown"} {
		res := getNoFollow(t, srv.URL+path)
		if res.StatusCode != http.StatusNotFound || res.Header.Get("Location") != "" || len(res.Cookies()) != 0 {
			t.Errorf("FAIL: %s = %d location %q cookies %d; want 404, no redirect, no cookie",
				path, res.StatusCode, res.Header.Get("Location"), len(res.Cookies()))
		}
		if res.Header.Get("Referrer-Policy") != "no-referrer" {
			t.Errorf("FAIL: %s lacks Referrer-Policy: no-referrer", path)
		}
	}
}

// A FORGE WHOSE SESSIONS CANNOT REDEEM ANSWERS EVERY LINK AS SPENT.
func TestSigninLinkWithoutRedeemerIsSpent(t *testing.T) {
	srv := signinServer(t, &betaSessions{accts: newBetaAccts()})
	res := getNoFollow(t, srv.URL+"/signin/good")
	if res.StatusCode != http.StatusNotFound || strings.Contains(res.Header.Get("Set-Cookie"), sessionCookie) {
		t.Errorf("FAIL: got %d with cookie %q; want 404 and none", res.StatusCode, res.Header.Get("Set-Cookie"))
	}
}

// testLetters is a capability set spelled out.
type testLetters string

func (l testLetters) HasAny(s string) bool { return strings.ContainsAny(string(l), s) }

// allowAll is a session plane whose one signed-in account is an admin
// everywhere: what the account could do, were it not on a token.
type allowAll struct{ *betaSessions }

func (allowAll) Resolve(string) (string, bool)       { return "agent", true }
func (allowAll) Allow(string, string, ops.Auth) bool { return true }

// A SESSION OPENED BY A LINK IS JUDGED ON ITS TOKEN AT EVERY ADMIN GATE,
// REPOSITORY AND FLEET; A PASSWORD SESSION OF THE SAME ACCOUNT IS NOT.
func TestLinkSessionAdminGatesFollowTheToken(t *testing.T) {
	sess := allowAll{&betaSessions{accts: newBetaAccts()}}
	verify := Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })
	onToken := func(letters string, repos ...string) *http.Request {
		req := httptest.NewRequest(http.MethodPost, "/", nil)
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "c"})
		tok := ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return testLetters(letters) },
			Reaches: func(repo string) bool { return len(repos) == 0 || repo != "" && repo == repos[0] }}
		return req.WithContext(ops.WithToken(req.Context(), tok))
	}
	plain := httptest.NewRequest(http.MethodPost, "/", nil)
	plain.AddCookie(&http.Cookie{Name: sessionCookie, Value: "c"})

	for _, repo := range []string{"quarry", ""} {
		if _, ok := gateAt(verify, sess, repo, ops.Admin, httptest.NewRecorder(), onToken("o")); ok {
			t.Errorf("FAIL: gateAt(%q, Admin) let a token ceilinged to o through", repo)
		}
		if mayAt(verify, sess, repo, ops.Admin, onToken("o")) {
			t.Errorf("FAIL: mayAt(%q, Admin) is true for a token ceilinged to o", repo)
		}
		if _, ok := gateAt(verify, sess, repo, ops.Admin, httptest.NewRecorder(), plain); !ok {
			t.Errorf("FAIL: gateAt(%q, Admin) refused a password session", repo)
		}
	}
	if _, ok := gateAt(verify, sess, "quarry", ops.Admin, httptest.NewRecorder(), onToken("oa")); !ok {
		t.Error("FAIL: a token holding a on the repository was refused")
	}
	if _, ok := gateAt(verify, sess, "other", ops.Admin, httptest.NewRecorder(), onToken("oa", "quarry")); ok {
		t.Error("FAIL: a token reached a repository it does not name")
	}
}

// NEXT CANNOT LEAVE THE SITE BY BACKSLASH OR CONTROL CHARACTER.
func TestSafeNextRefusesBackslashAndControls(t *testing.T) {
	for _, next := range []string{"/\\evil.com", "/\t/evil.com", "/a\x00b", "//evil.com", "https://evil.com"} {
		if got := safeNext(next); got != "/" {
			t.Errorf("FAIL: safeNext(%q) = %q; want /", next, got)
		}
	}
	if got := safeNext("/r/quarry?x=1"); got != "/r/quarry?x=1" {
		t.Errorf("FAIL: a local path was changed to %q", got)
	}
}

// A LINK OPENED IN A BROWSER THAT IS SIGNED IN KEEPS THAT SESSION IN THE
// STASH INSTEAD OF DROPPING IT.
func TestSigninLinkStashesTheSessionItReplaces(t *testing.T) {
	accts := newBetaAccts()
	_ = accts.AddAccount("pat", "pat-pw-long", false)
	sess := &linkSessions{betaSessions: &betaSessions{accts: accts}}
	srv := signinServer(t, sess)
	req, _ := http.NewRequest("GET", srv.URL+"/signin/good", nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "tok-pat"})
	client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
	res, err := client.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	var others string
	for _, c := range res.Cookies() {
		if c.Name == othersCookie {
			others = c.Value
		}
	}
	if !strings.Contains(others, "tok-pat") {
		t.Errorf("FAIL: the stash cookie %q lacks the replaced session", others)
	}
}