Skip to content
quarry Artifact [77aa081839]
Login

Artifact [77aa081839]

Artifact 77aa081839d55c81eab3d979805e04410c84e400f7241ccad41161bd110ccf8d:


// siteadmin_test.go proves #318 end to end: a site admin administers the
// forge, not the repositories under it. With no grant, a private
// repository does not exist for them on any door, a public one reads as
// it does for any signed-in account, and the lifecycle acts they keep
// are operator paths that leave a receipt with the owner.
package harness

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/chat"
	"craftdesign.group/quarry/internal/events"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ingest"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/web"
	"craftdesign.group/quarry/internal/zitcli"
)

const sitePW = "pw12345678"

type siteForge struct {
	srv *httptest.Server
	odb *oltp.DB
	reg *fakeRepoRegistry
}

// buildSiteForge wires the gates the way cmd/quarry does: the verifier
// asks MayAt, the read gate mirrors registrar.CanRead, the pages hide a
// private repository through WithPrivateRepos, and the registry's
// witness is the one main installs. alpha is public and vault private;
// dan administers and owns both, root is a site admin granted nothing,
// and plain is an ordinary account granted nothing.
func buildSiteForge(t *testing.T) siteForge {
	t.Helper()
	dataDir := t.TempDir()
	ev, err := events.Start(filepath.Join(dataDir, "nats"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { odb.Close() })
	room, err := chat.New(ev.Conn(), ev.JS())
	if err != nil {
		t.Fatal(err)
	}
	reg := &fakeRepoRegistry{}
	deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(nil), Chat: room,
		Accounts: harnessAccounts{db: odb}, Forum: odb, Votes: odb, ChatRefs: odb,
		Owners: odb, Acts: odb, Registry: reg}
	deps.CapsFor = func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
	private := func(repo string) bool {
		set, err := odb.PrivateRepos()
		return err != nil || set[repo]
	}
	deps.CanRead = func(ctx context.Context, repo string) bool {
		if !private(repo) {
			return true
		}
		user := ops.User(ctx)
		return user != "" && odb.EffectiveCaps(user, repo).HasAny("gjoh2r")
	}
	for name, vis := range map[string]string{"alpha": oltp.Public, "vault": oltp.Private} {
		path := buildStore(t, name+"seed")
		if err := odb.AddRepo(name, path, "", vis); err != nil {
			t.Fatal(err)
		}
		ix, err := index.Open(filepath.Join(dataDir, name+".db"))
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { ix.Close() })
		cli := zitcli.New(path)
		pipe := ingest.NewPipeline(cli, ev, ix, name, "harness")
		deps.Repos.Put(name, &api.Repo{Index: ix, Zit: cli, Sync: pipe.Sync, Code: name,
			Reindex: func() (int, error) { return 1, nil }})
		if _, err := pipe.Sync(); err != nil {
			t.Fatal(err)
		}
	}
	for _, a := range []struct {
		name  string
		admin bool
	}{{"root", true}, {"plain", false}, {"dan", false}, {"eve", false}} {
		if err := odb.AddAccount(a.name, sitePW, a.admin); err != nil {
			t.Fatal(err)
		}
	}
	for _, repo := range []string{"alpha", "vault"} {
		if err := odb.Grant("dan", repo, oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
		if err := odb.SetOwner(repo, "dan"); err != nil {
			t.Fatal(err)
		}
	}
	verify := func(repo, user, pass string, need ops.Auth) api.Decision {
		acct, ok := odb.Authenticate(user, pass)
		if !ok {
			return api.Denied
		}
		if odb.MayAt(acct, repo, need == ops.Admin) {
			return api.Allowed
		}
		return api.Insufficient
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	r.Observe(deps.WitnessOperator())
	apiRouter, err := api.RouterWithCaps(r, "0.0.0-harness", deps.CapsFor, deps.CanRead, verify)
	if err != nil {
		t.Fatal(err)
	}
	mux := http.NewServeMux()
	mux.Handle("/api/", apiRouter)
	mux.Handle("/", web.HandlerWith(r, nil, nil,
		[]web.Option{web.WithCaps(siteCapsPlane{harnessCaps{odb: odb}}), web.WithPrivateRepos(private),
			web.WithSessions(siteSessions{odb: odb})}, web.Verify(verify)))
	srv := httptest.NewServer(mux)
	t.Cleanup(srv.Close)
	return siteForge{srv: srv, odb: odb, reg: reg}
}

// siteCapsPlane is harnessCaps with the administration seam cmd/quarry's
// capsPlane gives the settings page, so its people list renders.
type siteCapsPlane struct{ harnessCaps }

func (c siteCapsPlane) CapHolders(repo string) ([]web.CapHolder, error) {
	rows, err := c.odb.CapHolders(repo)
	if err != nil {
		return nil, err
	}
	out := make([]web.CapHolder, 0, len(rows))
	for _, r := range rows {
		out = append(out, web.CapHolder{Account: r.Account, Caps: r.Caps, Team: r.Team, TeamCaps: r.TeamCaps})
	}
	return out, nil
}

func (c siteCapsPlane) CategoryCaps(repo, category string) string {
	return c.odb.CategoryCaps(repo, category)
}

func (c siteCapsPlane) SetCaps(actor, account, repo, caps string) (string, error) {
	return c.odb.SetCapsGuarded(actor, account, repo, caps)
}

func (c siteCapsPlane) SetCategoryCaps(repo, category, caps string) error {
	return c.odb.SetCategoryCaps(repo, category, caps)
}

// siteSessions is cmd/quarry's sessionPlane with the account name as the
// token: Allow asks MayAt, so a cookie is worth what a password is.
type siteSessions struct{ odb *oltp.DB }

func (s siteSessions) Login(user, password string) (string, time.Time, bool) {
	if _, ok := s.odb.Authenticate(user, password); !ok {
		return "", time.Time{}, false
	}
	return user, time.Now().Add(time.Hour), true
}

func (s siteSessions) Resolve(token string) (string, bool) {
	_, ok := s.odb.AccountByName(token)
	return token, ok
}

func (s siteSessions) Logout(string) {}

func (s siteSessions) Allow(user, repo string, need ops.Auth) bool {
	a, ok := s.odb.AccountByName(user)
	return ok && s.odb.MayAt(a, repo, need == ops.Admin)
}

// call speaks as a browser on a page and as an API client on /api/.
func (f siteForge) call(t *testing.T, user, method, path string, form url.Values) (int, string) {
	t.Helper()
	var body io.Reader
	if form != nil {
		body = strings.NewReader(form.Encode())
	}
	req, _ := http.NewRequest(method, f.srv.URL+path, body)
	if form != nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	if strings.HasPrefix(path, "/api/") {
		req.SetBasicAuth(user, sitePW)
	} else {
		req.AddCookie(&http.Cookie{Name: "quarry_session", Value: user})
	}
	client := f.srv.Client()
	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
	resp, err := client.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(b)
}

// Every door a private repository has answers a site admin with no
// grant as it answers a stranger: not found. On a public repository
// the same requests answer the site admin exactly as they answer an
// ordinary signed-in account.
func TestASiteAdminWithNoGrantGetsNoRepositoryDoor(t *testing.T) {
	f := buildSiteForge(t)
	doors := []struct {
		door, method, path string
		form               url.Values
	}{
		{"web page", "GET", "/r/%s/timeline", nil},
		{"web settings", "GET", "/r/%s/settings", nil},
		{"api read", "GET", "/api/v1/r/%s/timeline", nil},
		{"api write: forum post", "POST", "/api/v1/r/%s/forum", url.Values{"title": {"t"}, "body": {"b"}}},
		{"api write: landing accept", "POST", "/api/v1/r/%s/landing/1/accept", url.Values{}},
		{"api write: settings", "POST", "/api/v1/repos/update", url.Values{"description": {"mine now"}}},
		{"api write: grant themselves", "POST", "/api/v1/accounts/grant", url.Values{"name": {"root"}, "role": {"admin"}}},
	}
	at := func(path, repo string, form url.Values) (string, url.Values) {
		if form == nil {
			return strings.Replace(path, "%s", repo, 1), nil
		}
		out := url.Values{}
		for k, v := range form {
			out[k] = v
		}
		if !strings.Contains(path, "%s") {
			if path == "/api/v1/repos/update" {
				out.Set("name", repo)
			} else {
				out.Set("repo", repo)
			}
		}
		return strings.Replace(path, "%s", repo, 1), out
	}
	for _, d := range doors {
		path, form := at(d.path, "vault", d.form)
		if got, _ := f.call(t, "root", d.method, path, form); got != http.StatusNotFound {
			t.Errorf("FAIL %s: a site admin with no grant got %d on a private repository, want 404", d.door, got)
		}
		if d.method == "GET" {
			if got, _ := f.call(t, "dan", d.method, path, form); got == http.StatusNotFound {
				t.Errorf("FAIL %s: the private repository's own admin got 404; the door proves nothing", d.door)
			}
		}
		path, form = at(d.path, "alpha", d.form)
		plain, _ := f.call(t, "plain", d.method, path, form)
		root, _ := f.call(t, "root", d.method, path, form)
		if root != plain {
			t.Errorf("FAIL %s: on a public repository a site admin got %d where a signed-in account got %d", d.door, root, plain)
		}
	}
	if caps, _ := f.odb.CapsOf("root", "vault"); caps != "" {
		t.Errorf("FAIL: a refused grant was written: root holds %q on vault", caps)
	}
	// The push credential is derived from the check-in letter, and
	// with no grant there is none to derive.
	if f.odb.MayWrite("vault", "root") || f.odb.MayWrite("alpha", "root") {
		t.Error("FAIL: a site admin with no grant may push")
	}
}

// Search never consults a repository the caller may not read, and the
// dashboard is what the caller's grants reach.
func TestASiteAdminSearchesAndFollowsOnlyWhatTheyReach(t *testing.T) {
	f := buildSiteForge(t)
	type search struct {
		Results []struct {
			Repo string `json:"repo"`
		} `json:"results"`
	}
	hits := func(user string) map[string]bool {
		code, body := f.call(t, user, "GET", "/api/v1/search?q=vaultseed", nil)
		if code != http.StatusOK {
			t.Fatalf("search as %s: %d %s", user, code, body)
		}
		var out search
		if err := json.Unmarshal([]byte(body), &out); err != nil {
			t.Fatal(err)
		}
		seen := map[string]bool{}
		for _, h := range out.Results {
			seen[h.Repo] = true
		}
		return seen
	}
	if !hits("dan")["vault"] {
		t.Fatal("the private repository's own admin finds nothing; the search proves nothing")
	}
	if hits("root")["vault"] {
		t.Error("FAIL: a site admin with no grant found a private repository's content")
	}

	code, body := f.call(t, "root", "GET", "/api/v1/me/feed", nil)
	if code != http.StatusOK {
		t.Fatalf("me.feed: %d %s", code, body)
	}
	var feed struct {
		Repos []struct {
			Name string `json:"name"`
		} `json:"repos"`
		Feed []struct {
			Repo string `json:"repo"`
		} `json:"feed"`
	}
	if err := json.Unmarshal([]byte(body), &feed); err != nil {
		t.Fatal(err)
	}
	if len(feed.Repos) != 0 || len(feed.Feed) != 0 {
		t.Errorf("FAIL: a site admin with no grant is shown repositories %v and %d events", feed.Repos, len(feed.Feed))
	}
}

// The people list on a repository's settings is its members. A site
// admin is not one unless granted a role.
func TestTheRosterDoesNotListSiteAdmins(t *testing.T) {
	f := buildSiteForge(t)
	code, body := f.call(t, "dan", "GET", "/r/alpha/settings", nil)
	if code != http.StatusOK {
		t.Fatalf("settings as the repository's admin: %d", code)
	}
	if !strings.Contains(body, `/people/dan"`) {
		t.Fatal("the roster does not list the repository's own admin; the check proves nothing")
	}
	if strings.Contains(body, `/people/root"`) {
		t.Error("FAIL: the roster lists a site admin who holds no role here")
	}
}

// The lifecycle acts a site admin keeps are operator paths: they work
// on a repository the admin holds nothing on, they reveal none of its
// content, and each leaves a receipt in the owner's forge log.
func TestOperatorActsWorkWithoutAGrantAndLeaveAReceipt(t *testing.T) {
	f := buildSiteForge(t)
	receipt := func(repo, op string) bool {
		acts, err := f.odb.ForgeActs(repo, 50)
		if err != nil {
			t.Fatal(err)
		}
		for _, a := range acts {
			if a.Op == op && a.Actor == "root" && a.Owner == "dan" {
				return true
			}
		}
		return false
	}

	if code, body := f.call(t, "root", "POST", "/api/v1/r/vault/reindex", url.Values{}); code != http.StatusOK {
		t.Errorf("FAIL: a site admin could not reindex a private repository: %d %s", code, body)
	} else if !receipt("vault", "repo.reindex") {
		t.Error("FAIL: reindexing left the owner no receipt")
	}

	// A transfer on request names a recipient who already administers
	// the repository. Taking the deed for themselves is not an operator
	// act: it would make the operator a party to the repository.
	if code, _ := f.call(t, "root", "POST", "/api/v1/r/vault/claim", url.Values{}); code == http.StatusOK {
		t.Error("FAIL: a site admin with no grant took the deed to a private repository for themselves")
	}
	if err := f.odb.Grant("eve", "vault", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if code, body := f.call(t, "root", "POST", "/api/v1/r/vault/claim", url.Values{"account": {"eve"}}); code != http.StatusOK {
		t.Errorf("FAIL: a site admin could not hand a private repository to its other admin: %d %s", code, body)
	} else {
		if owner, _ := f.odb.OwnerOf("vault"); owner != "eve" {
			t.Errorf("FAIL: the transfer did not land: vault is held by %q", owner)
		}
		if !receipt("vault", "repo.claim") {
			t.Error("FAIL: the transfer left the owner no receipt")
		}
	}

	if code, body := f.call(t, "root", "POST", "/api/v1/repos/delete",
		url.Values{"name": {"alpha"}, "confirm": {"alpha"}}); code != http.StatusOK {
		t.Errorf("FAIL: a site admin could not delete a repository they do not own: %d %s", code, body)
	} else {
		if len(f.reg.deleted) != 1 || f.reg.deleted[0] != "alpha" {
			t.Errorf("FAIL: delete was not carried out: %v", f.reg.deleted)
		}
		if !receipt("alpha", "repo.delete") {
			t.Error("FAIL: deleting left the owner no receipt")
		}
	}

	// The operator path is the site admin's alone: an ordinary account
	// with no role is still refused every one of them.
	for _, c := range []struct{ path string }{{"/api/v1/r/vault/reindex"}, {"/api/v1/r/alpha/claim"}} {
		if code, _ := f.call(t, "plain", "POST", c.path, url.Values{"account": {"eve"}}); code == http.StatusOK {
			t.Errorf("FAIL: an ordinary account with no role passed the operator path %s", c.path)
		}
	}
}

// Abuse handling is an operator act where it needs no private content:
// a public repository's posts are public, so a site admin may hide one
// there with a receipt to the owner. On a private repository the post
// would have to be read to be judged, so there is no operator path.
func TestAnOperatorHidesAbuseOnlyWhereItIsPublic(t *testing.T) {
	f := buildSiteForge(t)
	post := func(repo string) string {
		code, body := f.call(t, "dan", "POST", "/api/v1/r/"+repo+"/forum",
			url.Values{"title": {"a thread"}, "body": {"something abusive"}})
		if code != http.StatusOK {
			t.Fatalf("posting to %s: %d %s", repo, code, body)
		}
		var out struct {
			Hash string `json:"hash"`
		}
		if err := json.Unmarshal([]byte(body), &out); err != nil || out.Hash == "" {
			t.Fatalf("posting to %s gave no hash: %s", repo, body)
		}
		return out.Hash
	}
	hide := func(repo, hash string) int {
		code, _ := f.call(t, "root", "POST", "/api/v1/r/"+repo+"/forum-hide",
			url.Values{"post": {hash}, "action": {"hide"}, "reason": {"abuse"}})
		return code
	}
	hash := post("alpha")
	if got := hide("alpha", hash); got != http.StatusOK {
		t.Errorf("FAIL: a site admin could not hide an abusive post on a public repository: %d", got)
	}
	// Hiding is abuse handling; restoring a post is the repository's
	// moderators' call, so the operator path stops at hide.
	if code, _ := f.call(t, "root", "POST", "/api/v1/r/alpha/forum-hide",
		url.Values{"post": {hash}, "action": {"unhide"}, "reason": {"changed my mind"}}); code == http.StatusOK {
		t.Error("FAIL: a site admin with no role unhid a post")
	}
	acts, err := f.odb.ForgeActs("alpha", 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(acts) != 1 || acts[0].Op != "forum.hide" || acts[0].Owner != "dan" {
		t.Errorf("FAIL: hiding left the owner no receipt: %+v", acts)
	}
	if got := hide("vault", post("vault")); got != http.StatusNotFound {
		t.Errorf("FAIL: a site admin hiding a post on a private repository got %d, want 404", got)
	}
	if code, _ := f.call(t, "root", "POST", "/api/v1/r/alpha/forum-queue",
		url.Values{"id": {"1"}, "action": {"approve"}}); code == http.StatusOK {
		t.Error("FAIL: a site admin with no role resolved a held submission, which is unpublished content")
	}
}

// The fleet page is gated at the forge, and its mirror forms act on one
// named repository. Reconfiguring, bridging, detaching or opening a
// mirror to writes is that repository's business, so each form asks for
// `a` on it and a site admin with no role there is refused before the op
// runs (#318).
func TestTheFleetMirrorFormsAskTheRepositorysLetter(t *testing.T) {
	f := buildSiteForge(t)
	for _, c := range []struct {
		path string
		form url.Values
	}{
		{"/fleet/mirror", url.Values{"repo": {"vault"}, "url": {"https://upstream.example/x"}}},
		{"/fleet/mirror/chatbridge", url.Values{"repo": {"vault"}, "detach": {"1"}}},
		{"/fleet/mirror/detach", url.Values{"repo": {"vault"}, "ack": {"fork"}}},
		{"/fleet/mirror/writes", url.Values{"repo": {"vault"}, "accept": {"true"}}},
	} {
		if got, _ := f.call(t, "root", "POST", c.path, c.form); got != http.StatusForbidden && got != http.StatusNotFound {
			t.Errorf("FAIL: %s answered a site admin with no role on vault %d, want a refusal before the op", c.path, got)
		}
	}
}