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 }
func (l *deleteLog) Add(api.RepoSpec) (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) 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}
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, 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; 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")
}
}
// 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)
}
}