Skip to content
quarry Artifact [f1a6b3c934]
Login

Artifact [f1a6b3c934]

Artifact f1a6b3c934b7daa1edeb4560811b3b76eca4c336c77bfd8566a354b97541152a:


package main

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

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

// deleteLog is a RepoRegistry that records deletes and does nothing
// else, so the site-scope tests run the real ops and doors without a
// store on disk.
type deleteLog struct {
	deleted, removed []string
	added            []api.RepoSpec
}

func (l *deleteLog) Add(s api.RepoSpec) (string, string, error) {
	l.added = append(l.added, s)
	return s.Name, "", nil
}
func (l *deleteLog) Owner(author string) (string, error)               { return author, nil }
func (l *deleteLog) NameFree(string, string, string) error             { return nil }
func (l *deleteLog) Remove(name string) error                          { l.removed = append(l.removed, name); return nil }
func (l *deleteLog) Update(string, api.RepoEdit, string, string) error { return nil }
func (l *deleteLog) Imports() ([]api.RepoImport, error)                { return nil, nil }
func (l *deleteLog) ConfirmImport(string, string) error                { return nil }
func (l *deleteLog) Delete(name, confirm string) error {
	l.deleted = append(l.deleted, name)
	return nil
}

// siteRig is zeitforge in miniature behind its real doors: dmestas a
// site admin, carol another, alice a person, zeitgeist a forge agent
// holding the flag. alice owns pub (public), secret (private) and
// granted (private, zeitgeist reads it by grant).
type siteRig struct {
	db     *oltp.DB
	h      http.Handler
	regs   *deleteLog
	pulled *[]string
	rg     *registrar
}

func newSiteRig(t *testing.T) siteRig {
	t.Helper()
	rg := newTestRegistrar(t, 25)
	db := rg.odb
	for _, a := range []struct {
		name  string
		admin bool
	}{{"dmestas", true}, {"carol", true}, {"alice", false}, {"zeitgeist", true}} {
		if err := db.AddAccount(a.name, doorPassword, a.admin); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.SetForgeAgent("zeitgeist", "dmestas"); err != nil {
		t.Fatal(err)
	}
	repos := map[string]*api.Repo{}
	pulled := &[]string{}
	for _, r := range []struct{ name, vis string }{{"pub", oltp.Public}, {"secret", oltp.Private}, {"granted", oltp.Private}, {"orphan", oltp.Private}} {
		if err := db.AddRepo(r.name, t.TempDir(), "", r.vis); err != nil {
			t.Fatal(err)
		}
		// orphan is private and unclaimed: nobody holds its deed.
		if r.name != "orphan" {
			if err := db.SetOwner(r.name, "alice"); err != nil {
				t.Fatal(err)
			}
		}
		if err := db.Grant("alice", r.name, oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
		ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { ix.Close() })
		name := r.name
		repos[r.name] = &api.Repo{Index: ix, Mirror: func(string) (bool, error) {
			*pulled = append(*pulled, name)
			return false, nil
		}}
	}
	if err := db.Grant("zeitgeist", "granted", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	rg.refreshPrivate()
	doors := &tokenDoors{db: db}
	rg.doors = doors
	byPassword := func(repo, user, pass string, need ops.Auth) api.Decision {
		if _, ok := db.Authenticate(user, pass); !ok {
			return api.Denied
		}
		return api.Allowed
	}
	capsFor := func(account, repo string) api.CapSet { return db.EffectiveCaps(account, repo) }
	regs := &deleteLog{}
	deps := api.Deps{Version: "v", Repos: api.NewRepos(repos), Accounts: accountStore{db: db}, CapsFor: capsFor,
		Owners: db, Acts: db, CanRead: rg.CanRead, Registry: regs, SigninLinks: db, Mirrors: mirrorStore{db: db},
		Deletable: func(repo string) bool { return repo == "pub" },
		Takedowns: db, Held: rg.HeldRefusal, TakedownsChanged: rg.refreshPrivate, GitMirrors: db, FossilMirrors: db,
		Private: func() map[string]bool { s, _ := db.PrivateRepos(); return s }, Teams: db}
	h, err := buildHandler(deps, nil, nil, doors.verify(oltp.ScopeAPI, byPassword), doors.verify(oltp.ScopeWeb, byPassword),
		capsFor, capsPlane{db: db}, nil, sessionPlane{db: db}, nil, nil, nil, nil, nil, "", nil, nil, nil,
		// Addresses and the private set as production reads them (RFC
		// 0025): the page gate answers a hidden repository as a name
		// nobody holds only when it knows both.
		web.WithAddresses(*addressesOf(db)), web.WithPrivateRepos(rg.IsPrivate))
	if err != nil {
		t.Fatal(err)
	}
	return siteRig{db: db, h: doors.wrap(h), regs: regs, pulled: pulled, rg: rg}
}

func (g siteRig) mint(t *testing.T, scopes ...string) (string, oltp.Token) {
	t.Helper()
	v, tok, err := g.db.MintToken("zeitgeist", oltp.TokenSpec{Name: strings.Join(scopes, "+"), Scopes: scopes})
	if err != nil {
		t.Fatal(err)
	}
	return v, tok
}

// do sends one request as zeitgeist on a Basic token, or on a session
// cookie when pass is "".
func (g siteRig) do(method, path, pass, cookie string, body any) (int, string) {
	return g.doAs("zeitgeist", method, path, pass, cookie, body)
}

func (g siteRig) doAs(user, method, path, pass, cookie string, body any) (int, string) {
	var rd io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		rd = strings.NewReader(string(b))
	}
	req := httptest.NewRequest(method, path, rd)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if pass != "" {
		req.SetBasicAuth(user, pass)
	}
	if cookie != "" {
		req.AddCookie(&http.Cookie{Name: web.SessionCookie, Value: cookie})
	}
	w := httptest.NewRecorder()
	g.h.ServeHTTP(w, req)
	return w.Code, w.Body.String()
}

// session opens a browser session through the token's one-time link.
func (g siteRig) session(t *testing.T, tok oltp.Token) string {
	t.Helper()
	link, _, err := g.db.MintSigninLink("zeitgeist", tok.ID, "/")
	if err != nil {
		t.Fatal(err)
	}
	cookie, _, _, ok := g.db.RedeemSigninLink(link, "", "")
	if !ok {
		t.Fatal("the link opened no session")
	}
	return cookie
}

// A site token reaches the operator's surfaces at both doors, and a
// session opened by its link does too. So does a forge agent's token
// without the site scope, and its link session: the account's flag
// decides. Neither once the token is revoked or the flag is gone.
func TestASiteTokenReachesTheOperatorsSurfaces(t *testing.T) {
	g := newSiteRig(t)
	site, siteTok := g.mint(t, oltp.ScopeAPI, oltp.ScopeWeb, oltp.ScopeSite)
	plain, plainTok := g.mint(t, oltp.ScopeAPI, oltp.ScopeWeb)
	siteCookie, plainCookie := g.session(t, siteTok), g.session(t, plainTok)
	surfaces := []string{"/api/v1/fleet", "/api/v1/ops/history", "/api/v1/accounts", "/fleet", "/fleet/ops/history", "/fleet/people"}
	for _, path := range surfaces {
		if code, body := g.do(http.MethodGet, path, site, "", nil); code != http.StatusOK {
			t.Errorf("FAIL: the site token at %s: %d %s", path, code, body)
		}
		if code, body := g.do(http.MethodGet, path, plain, "", nil); code != http.StatusOK {
			t.Errorf("FAIL: a forge agent's token without the site scope at %s: %d %s", path, code, body)
		}
		if !strings.HasPrefix(path, "/api/") {
			if code, _ := g.do(http.MethodGet, path, "", siteCookie, nil); code != http.StatusOK {
				t.Errorf("FAIL: the site token's link session at %s: %d", path, code)
			}
			if code, _ := g.do(http.MethodGet, path, "", plainCookie, nil); code != http.StatusOK {
				t.Errorf("FAIL: a forge agent's plain token's link session at %s: %d", path, code)
			}
		}
	}
	if err := g.db.RevokeToken("zeitgeist", siteTok.ID); err != nil {
		t.Fatal(err)
	}
	if code, _ := g.do(http.MethodGet, "/api/v1/fleet", site, "", nil); code != http.StatusUnauthorized {
		t.Errorf("FAIL: the revoked site token at the API: %d", code)
	}
	if code, _ := g.do(http.MethodGet, "/fleet", "", siteCookie, nil); code == http.StatusOK {
		t.Errorf("FAIL: the revoked site token's session still reaches /fleet")
	}
}

// Site admin by token stops at a private repository the agent holds no
// grant on: not read, not listed, not deleted. A grant restores it, and
// someone else's public repository it may still delete (#359).
func TestASiteTokenStopsAtPrivateRepositoriesItHoldsNoGrantOn(t *testing.T) {
	g := newSiteRig(t)
	site, _ := g.mint(t, oltp.ScopeAPI, oltp.ScopeWeb, oltp.ScopeSite)
	if code, _ := g.do(http.MethodGet, "/api/v1/r/secret/info", site, "", nil); code != http.StatusNotFound {
		t.Errorf("FAIL: the site token read secret's info: %d", code)
	}
	if code, _ := g.do(http.MethodGet, "/api/v1/r/granted/info", site, "", nil); code == http.StatusNotFound {
		t.Errorf("FAIL: the site token cannot read granted, where it holds a grant: %d", code)
	}
	// The settings page answers secret exactly as it answers a name
	// nobody holds, and opens granted.
	nobody, _ := g.do(http.MethodGet, "/r/nonesuch/settings", site, "", nil)
	if code, _ := g.do(http.MethodGet, "/r/secret/settings", site, "", nil); code != nobody || code == http.StatusOK {
		t.Errorf("FAIL: the site token at secret's settings page: %d; a name nobody holds: %d", code, nobody)
	}
	if code, _ := g.do(http.MethodGet, "/r/granted/settings", site, "", nil); code != http.StatusOK {
		t.Errorf("FAIL: the site token cannot open granted's settings page, where it holds a grant: %d", code)
	}
	// A person with the flag is the operator as before: the fleet lists
	// every store to them.
	if _, body := g.doAs("dmestas", http.MethodGet, "/api/v1/fleet", doorPassword, "", nil); !strings.Contains(body, `"secret"`) {
		t.Errorf("FAIL: the fleet hides secret from a site admin on their password: %s", body)
	}
	for _, path := range []string{"/api/v1/fleet", "/api/v1/repos", "/api/v1/search?q=secret"} {
		code, body := g.do(http.MethodGet, path, site, "", nil)
		if code != http.StatusOK {
			t.Errorf("FAIL: %s: %d %s", path, code, body)
			continue
		}
		if strings.Contains(body, `"secret"`) {
			t.Errorf("FAIL: %s lists secret to the site token: %s", path, body)
		}
		if path == "/api/v1/fleet" && !strings.Contains(body, `"granted"`) {
			t.Errorf("FAIL: the fleet listing hides granted, where the site token holds a grant: %s", body)
		}
	}
	if code, body := g.do(http.MethodPost, "/api/v1/repos/delete", site, "", map[string]string{"name": "secret", "confirm": "secret"}); code != http.StatusNotFound {
		t.Errorf("FAIL: the site token's delete of secret: %d %s", code, body)
	}
	if code, body := g.do(http.MethodPost, "/api/v1/repos/delete", site, "", map[string]string{"name": "pub", "confirm": "pub"}); code != http.StatusOK {
		t.Errorf("FAIL: the site token could not delete alice's public repository: %d %s", code, body)
	}
	if strings.Join(g.regs.deleted, ",") != "pub" {
		t.Errorf("FAIL: deleted %v, want only pub", g.regs.deleted)
	}
}

// A site token is minted by a site admin on their own sign-in, for a
// forge agent holding the flag, and for nobody else.
func TestOnlyASiteAdminMintsASiteTokenForAFlaggedForgeAgent(t *testing.T) {
	g := newSiteRig(t)
	if err := g.db.NewForgeAgent("fbot", "dmestas"); err != nil {
		t.Fatal(err)
	}
	if err := g.db.NewAgent("alice-bot", "alice", -1); err != nil {
		t.Fatal(err)
	}
	mint := func(user, agent string) (int, string) {
		return g.doAs(user, http.MethodPost, "/api/v1/agents/tokens", doorPassword, "",
			map[string]any{"agent": agent, "name": "site", "scopes": "api,site"})
	}
	if code, body := mint("dmestas", "zeitgeist"); code != http.StatusOK {
		t.Fatalf("FAIL: a site admin could not mint zeitgeist a site token: %d %s", code, body)
	}
	for _, c := range []struct{ user, agent string }{{"dmestas", "fbot"}, {"dmestas", "alice-bot"}, {"alice", "alice-bot"}} {
		if code, body := mint(c.user, c.agent); code == http.StatusOK || !strings.Contains(body, "site") {
			t.Errorf("FAIL: %s minted %s a site token: %d %s", c.user, c.agent, code, body)
		}
	}
	site, _ := g.mint(t, oltp.ScopeAPI, oltp.ScopeSite)
	if code, body := g.do(http.MethodPost, "/api/v1/agents/tokens", site, "",
		map[string]any{"agent": "zeitgeist", "name": "again", "scopes": "api,site"}); code != http.StatusForbidden {
		t.Errorf("FAIL: a site token minted a site token: %d %s", code, body)
	}
}

// The #922 guards hold for a forge agent acting by its site token: it
// never deletes, disables or re-flags a site admin, and never makes a
// site admin or another forge agent.
func TestTheForgeAgentGuardsHoldUnderASiteToken(t *testing.T) {
	g := newSiteRig(t)
	site, _ := g.mint(t, oltp.ScopeAPI, oltp.ScopeSite)
	if code, body := g.do(http.MethodPost, "/api/v1/accounts", site, "", map[string]any{"name": "pal", "password": doorPassword}); code != http.StatusOK {
		t.Fatalf("FAIL: the site token could not add a person: %d %s", code, body)
	}
	for _, c := range []struct {
		path string
		body any
	}{
		{"/api/v1/accounts/delete", map[string]any{"name": "carol"}},
		{"/api/v1/accounts/disable", map[string]any{"name": "carol", "on": true}},
		{"/api/v1/accounts", map[string]any{"name": "boss", "password": doorPassword, "admin": true}},
		{"/api/v1/agents", map[string]any{"name": "fbot", "operator": "forge", "answerable": "dmestas"}},
		{"/api/v1/agents/tokens", map[string]any{"agent": "zeitgeist", "name": "again", "scopes": "api,site"}},
	} {
		if code, body := g.do(http.MethodPost, c.path, site, "", c.body); code != http.StatusForbidden {
			t.Errorf("FAIL: %s %v by the site token: %d %s", c.path, c.body, code, body)
		}
	}
	if a, ok := g.db.AccountByName("carol"); !ok || !a.Admin || a.Disabled != "" {
		t.Errorf("FAIL: carol reads %+v after the guarded acts", a)
	}
	if a, ok := g.db.AccountByName("boss"); ok && a.Admin {
		t.Errorf("FAIL: the site token made a site admin")
	}
	if g.db.IsForgeAgent("fbot") {
		t.Errorf("FAIL: the site token made a forge agent")
	}
}

// form posts a fleet page's form as zeitgeist's link session.
func (g siteRig) form(path, cookie string, v url.Values) (int, string) {
	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(v.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(&http.Cookie{Name: web.SessionCookie, Value: cookie})
	w := httptest.NewRecorder()
	g.h.ServeHTTP(w, req)
	return w.Code, w.Body.String()
}

// The fleet pages call their ops in-process, past the router, so each
// op holds the private-repository line itself. A site token's session
// learns nothing of a private store it holds no grant on from the
// remove button, does not un-serve an unclaimed one, pulls only the
// mirrors it can read, and mints no worker credential; a site token
// does not re-point the forge's storage.
func TestTheFleetPagesHoldThePrivateLineForASiteToken(t *testing.T) {
	g := newSiteRig(t)
	site, siteTok := g.mint(t, oltp.ScopeAPI, oltp.ScopeWeb, oltp.ScopeSite)
	cookie := g.session(t, siteTok)

	nobody, _ := g.form("/fleet/repo/remove", cookie, url.Values{"repo": {"nonesuch"}})
	for _, repo := range []string{"secret", "orphan"} {
		code, body := g.form("/fleet/repo/remove", cookie, url.Values{"repo": {repo}})
		if code != nobody || code == http.StatusSeeOther {
			t.Errorf("FAIL: removing %s answered %d; a name nobody holds answered %d", repo, code, nobody)
		}
		if strings.Contains(body, "alice") {
			t.Errorf("FAIL: removing %s named its owner: %s", repo, body)
		}
	}
	if len(g.regs.removed) != 0 {
		t.Errorf("FAIL: the site session un-served %v", g.regs.removed)
	}

	for _, repo := range []string{"pub", "secret"} {
		if err := g.db.SetMirror(repo, "https://example.invalid/"+repo, time.Hour); err != nil {
			t.Fatal(err)
		}
	}
	// Every mirror is the fleet page's button; the API names one store.
	code, body := g.form("/fleet/mirror/run", cookie, url.Values{"repo": {""}})
	if code != http.StatusOK || strings.Contains(body, "/r/secret") || !strings.Contains(body, "/r/pub") {
		t.Errorf("FAIL: every mirror, for the site session: %d, secret listed %v, pub listed %v",
			code, strings.Contains(body, "/r/secret"), strings.Contains(body, "/r/pub"))
	}
	// Not shown is not enough: the private store must not be pulled.
	if got := strings.Join(*g.pulled, ","); got != "pub" {
		t.Errorf("FAIL: the site session pulled %q, want only pub", got)
	}

	if code, _ := g.form("/fleet/settings/worker-tokens", cookie, url.Values{"name": {"w"}}); code != http.StatusForbidden {
		t.Errorf("FAIL: the site session at the worker-token form: %d, want 403", code)
	}
	if code, body := g.do(http.MethodPost, "/api/v1/fleet/bucket", site, "", map[string]string{"endpoint": "https://example.invalid", "bucket": "b", "accessKey": "k"}); code != http.StatusForbidden {
		t.Errorf("FAIL: the site token re-pointed the forge's storage: %d %s", code, body)
	}
}