Skip to content
quarry Artifact [3455f26312]
Login

Artifact [3455f26312]

Artifact 3455f26312863144d0f21a3fe7560f206cd9789a2ccde6aa8b80c2177ea14a5a:


package main

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

	"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 []string }

func (l *deleteLog) Add(api.RepoSpec) (string, error)                  { return "", nil }
func (l *deleteLog) Remove(string) error                               { 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) 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
}

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{}
	for _, r := range []struct{ name, vis string }{{"pub", oltp.Public}, {"secret", oltp.Private}, {"granted", oltp.Private}} {
		if err := db.AddRepo(r.name, t.TempDir(), "", r.vis); err != nil {
			t.Fatal(err)
		}
		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() })
		repos[r.name] = &api.Repo{Index: ix}
	}
	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}
	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)
	if err != nil {
		t.Fatal(err)
	}
	return siteRig{db: db, h: doors.wrap(h), regs: regs}
}

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; a forge agent's token without
// site does not, nor does the site token once 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, _ := g.do(http.MethodGet, path, plain, "", nil); code == http.StatusOK {
			t.Errorf("FAIL: a forge agent's token without site at %s: 200", path)
		}
		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 plain token's link session at %s: 200", path)
			}
		}
	}
	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")
	}
}