Skip to content
quarry Artifact [af53d09b86]
Login

Artifact [af53d09b86]

Artifact af53d09b860790fde85ef7e925c910884a1b3e9691e9afa7259032541599e545:


package harness

import (
	"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

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