Skip to content
quarry Artifact [7f542c4539]
Login

Artifact [7f542c4539]

Artifact 7f542c4539bd95262ddd5d425172d16ab25284bcb1afceaf64f5ec8b628cb50f:


package harness

import (
	"encoding/json"
	"io"
	"net/http"
	"path/filepath"
	"strings"
	"testing"

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

// TestCapsGateTheAPI is RFC 0004 §4 enforced end to end: each account
// holds exactly one fossil capability letter and may reach exactly the
// surfaces that letter opens. The Auth floor lets every authenticated
// caller through; the capability check is what decides.
func TestCapsGateTheAPI(t *testing.T) {
	stores := map[string]string{"alpha": buildStore(t, "capseed")}
	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, stores, dataDir,
		func(repo, user, pass string, need ops.Auth) api.Decision {
			if user == "" {
				return api.Denied
			}
			return api.Allowed
		})
	// The harness opened this same file; a second handle sees its rows.
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	must := func(err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	must(odb.AddAccount("reader", "pw12345678", false))
	must(odb.SetCaps("reader", "alpha", "o")) // Read
	must(odb.AddAccount("forumer", "pw12345678", false))
	must(odb.SetCaps("forumer", "alpha", "3")) // WrForum
	// This test is about letters, not the fleet default: RFC 0008's
	// Public project lets any signed-in account post in the forum, so
	// pin fossil's stock anonymous string to see the gate alone.
	must(odb.SetCategoryCaps("alpha", "anonymous", oltp.StockAnonymous))

	call := func(user, method, path, body string) int {
		var r io.Reader
		if body != "" {
			r = strings.NewReader(body)
		}
		req, _ := http.NewRequest(method, srv.URL+path, r)
		req.SetBasicAuth(user, "pw12345678")
		if body != "" {
			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		}
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		resp.Body.Close()
		return resp.StatusCode
	}

	// o reads.
	if got := call("reader", "GET", "/api/v1/r/alpha/timeline", ""); got != http.StatusOK {
		t.Fatalf("o cannot read the timeline: %d", got)
	}
	// o is refused the forum write surface; 3 is not.
	if got := call("reader", "POST", "/api/v1/r/alpha/forum",
		"title=x&body=y"); got != http.StatusForbidden {
		t.Fatalf("o opened the forum write surface: %d", got)
	}
	if got := call("forumer", "POST", "/api/v1/r/alpha/forum",
		"title=from+the+forum+cap&body=hello"); got != http.StatusOK {
		t.Fatalf("3 cannot write the forum: %d", got)
	}
	// 3 does not carry repository administration (a).
	if got := call("forumer", "POST", "/api/v1/repos",
		"name=nope&create=1"); got == http.StatusOK {
		t.Fatal("3 reached repository creation")
	}

	// The WEB forms answer to the same letters as the API — a page that
	// gates more loosely than its op is a hole with a nice layout.
	if got := call("reader", "POST", "/r/alpha/forum-new",
		"title=x&body=y"); got != http.StatusForbidden {
		t.Fatalf("o posted a thread through the web form: %d", got)
	}
	// 3 posts successfully: a redirect to the new thread, or the page
	// itself — either way, NOT a refusal.
	if got := call("forumer", "POST", "/r/alpha/forum-new",
		"title=web+thread&body=hello"); got == http.StatusForbidden || got == http.StatusUnauthorized {
		t.Fatalf("3 cannot post through the web form: %d", got)
	}
	// Ticket filing needs n — which the ANONYMOUS category grants to
	// every signed-in account (fossil's hmnc default). So this passes,
	// and that is the rule, not a leak: a signed-in human may file a
	// ticket unless the repo says otherwise.
	if got := call("forumer", "POST", "/r/alpha/ticket/new",
		"title=t&comment=c"); got == http.StatusForbidden {
		t.Fatalf("the anonymous category did not grant n: %d", got)
	}
	// Take n away from the category and the same request is refused —
	// which is what proves the gate reads caps rather than waving
	// signed-in callers through.
	if err := odb.SetCategoryCaps("alpha", "anonymous", "h"); err != nil {
		t.Fatal(err)
	}
	if got := call("forumer", "POST", "/r/alpha/ticket/new",
		"title=t2&comment=c"); got != http.StatusForbidden {
		t.Fatalf("ticket filing survived the loss of n: %d", got)
	}
}

// TestCategoryCapsReachTheSurfaces: an account with NO explicit caps
// still reaches what the anonymous category grants, and a repo taken
// private (blank nobody+anonymous) closes even the read surfaces —
// fossil's Security-Audit behavior, which is quarry's invisibility.
func TestCategoryCapsReachTheSurfaces(t *testing.T) {
	stores := map[string]string{"alpha": buildStore(t, "catseed")}
	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, stores, dataDir,
		func(repo, user, pass string, need ops.Auth) api.Decision {
			if user == "" {
				return api.Denied
			}
			return api.Allowed
		})
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	if err := odb.AddAccount("plain", "pw12345678", false); err != nil {
		t.Fatal(err)
	}
	get := func(user, path string) int {
		req, _ := http.NewRequest("GET", srv.URL+path, nil)
		if user != "" {
			req.SetBasicAuth(user, "pw12345678")
		}
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		resp.Body.Close()
		return resp.StatusCode
	}
	// Stock nobody carries o: the timeline reads for everyone.
	if got := get("plain", "/api/v1/r/alpha/timeline"); got != http.StatusOK {
		t.Fatalf("category caps do not reach the timeline: %d", got)
	}
	// Take it private: blank the two visitor categories.
	if err := odb.SetCategoryCaps("alpha", "nobody", ""); err != nil {
		t.Fatal(err)
	}
	if err := odb.SetCategoryCaps("alpha", "anonymous", ""); err != nil {
		t.Fatal(err)
	}
	if got := get("plain", "/api/v1/r/alpha/timeline"); got == http.StatusOK {
		t.Fatal("a private repo still served its timeline to a capless account")
	}
}

// Capability e end to end: fossil withholds private_contact from the
// public and shows it to holders of e. quarry dropped it outright
// because it had no capability to gate on; it has one now, so the
// field must reach a holder and nobody else.
//
// The unit test covers the render rule. This covers the thing that
// actually decides it — a real caps plane answering for a real account.
func TestPrivateContactReachesOnlyHoldersOfE(t *testing.T) {
	// The realistic source of this field is an IMPORT: a fossil repo
	// hosted here brings its own private_contact values with it. quarry
	// does not author the field, so the test writes it the way a mirror
	// would — into the store, before the index ever sees it.
	store := buildStore(t, "eseed")
	zrun(t, store, "ticket", "new", "--user", "eseed",
		"title=a bug with a contact", "status=Open",
		"[email protected]")

	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, dataDir,
		func(repo, user, pass string, need ops.Auth) api.Decision {
			if user == "" {
				return api.Denied
			}
			return api.Allowed
		})
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	for _, u := range []string{"plain", "holder"} {
		if err := odb.AddAccount(u, "pw12345678", false); err != nil {
			t.Fatal(err)
		}
	}
	// Only one of them may see private contact details.
	if err := odb.SetCaps("holder", "alpha", "re"); err != nil {
		t.Fatal(err)
	}
	if err := odb.SetCaps("plain", "alpha", "r"); err != nil {
		t.Fatal(err)
	}
	get := func(user, path string) string {
		req, _ := http.NewRequest("GET", srv.URL+path, nil)
		req.SetBasicAuth(user, "pw12345678")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		return string(b)
	}

	var list struct {
		Tickets []struct {
			ID    string `json:"id"`
			Title string `json:"title"`
		} `json:"tickets"`
	}
	if err := json.Unmarshal([]byte(get("holder", "/api/v1/r/alpha/ticket?state=all&limit=50")), &list); err != nil {
		t.Fatal(err)
	}
	var id string
	for _, tk := range list.Tickets {
		if strings.Contains(tk.Title, "a contact") {
			id = tk.ID
		}
	}
	if id == "" {
		t.Fatalf("the imported ticket never reached the index: %+v", list.Tickets)
	}

	page := "/r/alpha/ticket/" + id
	if body := get("plain", page); strings.Contains(body, "[email protected]") {
		t.Error("a reader without e was shown a private contact address")
	}
	if body := get("holder", page); !strings.Contains(body, "[email protected]") {
		t.Error("a holder of e was refused the field the capability names")
	}
}

// A ticket its own tracker marked private. quarry hid it from
// everyone, including the people it was for, because there was no
// capability to grant an exception to. Fossil shows it to holders of e.
//
// The failure to avoid is not just "shown to the wrong reader" but
// "confirmed to exist": without e the ticket must not resolve at all,
// must not be counted, and must not appear under any facet — a listing
// that says 4 while showing 3 has told a stranger a private ticket is
// there.
func TestPrivateTicketsExistOnlyForHoldersOfE(t *testing.T) {
	store := buildStore(t, "privseed")
	zrun(t, store, "ticket", "new", "--user", "privseed",
		"title=a private matter", "status=Open", "is_private=1")

	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, dataDir,
		func(repo, user, pass string, need ops.Auth) api.Decision {
			if user == "" {
				return api.Denied
			}
			return api.Allowed
		})
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	for _, u := range []string{"plain", "holder"} {
		if err := odb.AddAccount(u, "pw12345678", false); err != nil {
			t.Fatal(err)
		}
	}
	if err := odb.SetCaps("holder", "alpha", "re"); err != nil {
		t.Fatal(err)
	}
	if err := odb.SetCaps("plain", "alpha", "r"); err != nil {
		t.Fatal(err)
	}
	list := func(user string) (titles []string, open int) {
		req, _ := http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/ticket?state=all&limit=50", nil)
		req.SetBasicAuth(user, "pw12345678")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		var out struct {
			Tickets []struct{ Title string } `json:"tickets"`
			Open    int                      `json:"open"`
		}
		json.NewDecoder(resp.Body).Decode(&out)
		for _, tk := range out.Tickets {
			titles = append(titles, tk.Title)
		}
		return titles, out.Open
	}

	plainTitles, plainOpen := list("plain")
	for _, title := range plainTitles {
		if strings.Contains(title, "private matter") {
			t.Error("a private ticket was listed to a reader without e")
		}
	}
	heldTitles, heldOpen := list("holder")
	var found bool
	for _, title := range heldTitles {
		if strings.Contains(title, "private matter") {
			found = true
		}
	}
	if !found {
		t.Errorf("a holder of e cannot see the private ticket: %v", heldTitles)
	}
	// The count must agree with the list each reader is shown. A number
	// that includes what the listing hides is a confirmation leak.
	if plainOpen != len(plainTitles) || heldOpen != len(heldTitles) {
		t.Errorf("counts disagree with listings: plain %d/%d, holder %d/%d",
			plainOpen, len(plainTitles), heldOpen, len(heldTitles))
	}
	if heldOpen <= plainOpen {
		t.Errorf("the private ticket is in nobody's count: plain %d, holder %d", plainOpen, heldOpen)
	}
}

// Who may land a branch. zit#127 scoped approvals out, correctly, for a CLI
// where the operator IS the authority; a multi-account forge has to answer it.
//
// The rule: accept rides `i` — fossil's Write/check-in letter — because accept
// MERGES, and everything else about a landing is a forum artifact. Speaking in
// the discussion is `3`; settling it with a check-in is `i`. RFC 0004 stored
// `i` and left it inert; this is the surface that wakes it.
//
// And there is deliberately NO rule against accepting your own landing. Inside
// a cathedral the author already holds the commit bit, and a required second
// pair of eyes is a policy fossil has no letter for — inventing one here would
// be quarry policy wearing fossil's clothes. The test pins that absence so
// nobody adds the rule back as an obvious-looking improvement.
func TestLandingAcceptRidesTheCheckinCapability(t *testing.T) {
	stores := map[string]string{"alpha": buildStore(t, "landseed")}
	dataDir := t.TempDir()
	srv := ingestAndServeAuth(t, stores, dataDir,
		func(repo, user, pass string, need ops.Auth) api.Decision {
			if user == "" {
				return api.Denied
			}
			return api.Allowed
		})
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	for _, u := range []string{"talker", "lander"} {
		if err := odb.AddAccount(u, "pw12345678", false); err != nil {
			t.Fatal(err)
		}
	}
	// talker may read and speak in the thread but not write check-ins;
	// lander may do both.
	if err := odb.SetCaps("talker", "alpha", "23"); err != nil {
		t.Fatal(err)
	}
	if err := odb.SetCaps("lander", "alpha", "23i"); err != nil {
		t.Fatal(err)
	}
	code := func(user, path string) int {
		req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(`{"message":"x"}`))
		req.Header.Set("Content-Type", "application/json")
		req.SetBasicAuth(user, "pw12345678")
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		resp.Body.Close()
		return resp.StatusCode
	}

	// Forbidden is the capability speaking. Anything else — 404 for a
	// missing record, 500 from the store — means the gate never ran, so
	// this asserts the gate and not the outcome behind it.
	if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/accept"); got != http.StatusForbidden {
		t.Errorf("accept without i: %d, want 403 — the check-in capability is what settles a landing", got)
	}
	if got := code("lander", "/api/v1/r/alpha/landing/deadbeef/accept"); got == http.StatusForbidden {
		t.Error("a holder of i was refused the accept surface")
	}
	// Rejecting writes no check-in, so it must NOT need i.
	if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/reject"); got == http.StatusForbidden {
		t.Error("reject demanded the check-in capability; it merges nothing")
	}
	if got := code("talker", "/api/v1/r/alpha/landing/deadbeef/comment"); got == http.StatusForbidden {
		t.Error("commenting on a landing demanded more than the forum capability")
	}
}