Skip to content
quarry Artifact [cb814dc63a]
Login

Artifact [cb814dc63a]

Artifact cb814dc63a840316a80365db2eb5af9d42ffb2b21d56c2b9ac252f360589d7e8:


package api

import (
	"context"
	"errors"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// A name that is not an account is not found, in words naming it, on
// both grant doors; never an internal error.
func TestGrantingAMissingAccountIsNotFound(t *testing.T) {
	r, _, _ := agentCoreRig(t, 5)
	_, err := r.Invoke(asUser("dmestas"), "account.grant", accountGrantIn{Name: "zoot", Repo: "test", Role: "developer"})
	if !errors.Is(err, ops.ErrNotFound) || !strings.Contains(err.Error(), "zoot") {
		t.Errorf("FAIL: account.grant on a missing account: %v", err)
	}
	_, err = r.Invoke(asUser("dmestas"), "account.caps", accountCapsIn{Account: "zoot", Repo: "test", Caps: "gjoh"})
	if !errors.Is(err, ops.ErrNotFound) || !strings.Contains(err.Error(), "zoot") {
		t.Errorf("FAIL: account.caps on a missing account: %v", err)
	}
	t.Log("ok")
}

// Naming something that is not an agent is not found; an agent somebody
// else operates stays forbidden.
func TestAgentOpsOnANameWithNoOperator(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if err := db.NewAgent("jva-bot", "jva", -1); err != nil {
		t.Fatal(err)
	}
	for _, c := range []struct {
		op string
		in any
	}{
		{"agent.grant", agentGrantIn{Agent: "zoot", Repo: "test", Role: "developer"}},
		{"agent.inherit", agentInheritIn{Agent: "zoot", On: true}},
		{"agent.ask", agentAskIn{Agent: "zoot", On: true}},
		{"agent.ask", agentAskIn{Agent: "mallory", On: true}}, // a person
	} {
		if _, err := r.Invoke(asUser("dmestas"), c.op, c.in); !errors.Is(err, ops.ErrNotFound) {
			t.Errorf("FAIL: %s on a name with no operator: %v", c.op, err)
		}
	}
	for _, c := range []struct {
		op string
		in any
	}{
		{"agent.grant", agentGrantIn{Agent: "jva-bot", Repo: "test", Role: "developer"}},
		{"agent.inherit", agentInheritIn{Agent: "jva-bot", On: true}},
		{"agent.ask", agentAskIn{Agent: "jva-bot", On: true}},
	} {
		if _, err := r.Invoke(asUser("dmestas"), c.op, c.in); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: %s on somebody else's agent: %v", c.op, err)
		}
	}
	t.Log("ok")
}

// A repository's own agent is its admin's to put to work from the one
// door; a stranger, a token and the clip all hold.
func TestARepositoryAdminGrantsItsOwnAgent(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if err := db.NewDedicatedAgent("suit-bot", "repo:test", "dmestas"); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("jva", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	in := agentGrantIn{Agent: "suit-bot", Repo: "test", Role: "developer"}

	if _, err := r.Invoke(asUser("mallory"), "agent.grant", in); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a stranger granted a repository's agent: %v", err)
	}
	if held := db.EffectiveCaps("suit-bot", "test"); held.HasAny("i") {
		t.Error("FAIL: the stranger's grant took effect")
	}
	onToken := ops.WithToken(asUser("jva"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
	if _, err := r.Invoke(onToken, "agent.grant", in); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a token granted: %v", err)
	}
	out, err := r.Invoke(asUser("jva"), "agent.grant", in)
	if err != nil {
		t.Fatalf("FAIL: the repository's admin could not grant: %v", err)
	}
	if got := out.(agentGrantOut); got.Clipped || got.Caps == "" {
		t.Errorf("FAIL: the grant: %+v", got)
	}
	if !db.EffectiveCaps("suit-bot", "test").HasAny("i") {
		t.Error("FAIL: the agent holds no check-in letter after the grant")
	}
	// Admin is asked for; the agent never holds a.
	if _, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: "suit-bot", Repo: "test", Role: "admin"}); err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	if db.EffectiveCaps("suit-bot", "test").HasAny("a") {
		t.Error("FAIL: a repository's agent holds the admin letter")
	}
	t.Log("ok")
}

// A repository's or a team's agent holds only inside its scope and
// only what somebody who administers the repository gives it, because
// it has no operator ceiling and would keep a grant after its giver
// lost their letters. Judged in the op and again in the store.
func TestADedicatedAgentIsGrantedOnlyInScopeAndByAnAdminThere(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if err := db.AddRepo("other", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := db.AddTeam("crew", "Crew", "", "jva"); err != nil {
		t.Fatal(err)
	}
	for _, a := range [][2]string{{"o-bot", "repo:other"}, {"crew-bot", "team:crew"}} {
		if err := db.NewDedicatedAgent(a[0], a[1], "jva"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.Grant("jva", "other", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	grant := func(agent, repo, role string) error {
		_, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: agent, Repo: repo, Role: role})
		return err
	}

	// A repository's agent: not on a repository but its own, however
	// much its manager reaches. The op refuses, and so does the store.
	if err := db.Grant("jva", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if err := grant("o-bot", "test", "developer"); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: a repository's agent was granted on another repository: %v", err)
	}
	if _, _, err := db.AgentGrant("jva", "o-bot", "test", oltp.Write.Caps()); !oltp.IsRefusal(err) {
		t.Errorf("FAIL: the store wrote a grant outside the agent's scope: %v", err)
	}
	if stored, _ := db.CapsOf("o-bot", "test"); stored != "" {
		t.Errorf("FAIL: a row was written outside the scope: %q", stored)
	}

	// A team's agent: only a repository the team holds.
	if err := grant("crew-bot", "test", "developer"); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: a team's agent was granted on a repository the team does not hold: %v", err)
	}
	if err := db.SetTeamRepo("crew", "test", "jva"); err != nil {
		t.Fatal(err)
	}
	// ...and only by somebody who administers it, not a team owner who
	// is a developer there.
	if err := db.Grant("jva", "test", oltp.Write); err != nil {
		t.Fatal(err)
	}
	if err := grant("crew-bot", "test", "developer"); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a team owner without the admin letter granted its agent: %v", err)
	}
	if _, _, err := db.AgentGrant("jva", "crew-bot", "test", oltp.Write.Caps()); !oltp.IsRefusal(err) {
		t.Errorf("FAIL: the store let a non-admin grant a team's agent: %v", err)
	}
	if err := db.Grant("jva", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	// The store does not take the API's word that the giver answers for
	// the scope: an admin of the repository who is not on the team is
	// refused there.
	if err := db.Grant("mallory", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.AgentGrant("mallory", "crew-bot", "test", oltp.Write.Caps()); !oltp.IsRefusal(err) {
		t.Errorf("FAIL: the store let a non-owner of the team grant its agent: %v", err)
	}
	if err := grant("crew-bot", "test", "developer"); err != nil {
		t.Fatalf("FAIL: the repository's admin could not grant the team's agent: %v", err)
	}
	// Demoted, the giver can still take it away.
	if err := db.Grant("jva", "test", oltp.Write); err != nil {
		t.Fatal(err)
	}
	if err := grant("crew-bot", "test", ""); err != nil {
		t.Errorf("FAIL: a demoted giver could not remove the grant: %v", err)
	}
	t.Log("ok")
}

// A request is checked when it is asked: one the owner could not run
// is not asked, and the agent is left with exactly the refusal its
// plain call got, whichever way it failed (a missing agent, somebody
// else's, a missing repository, one the agent cannot read), so asking
// cannot probe. One they could run is recorded.
func TestASignOffIsCheckedWhenItIsAsked(t *testing.T) {
	r, _, _, _, _ := signOffRig(t)
	h, err := RouterWithCaps(r, "v", func(string, string) CapSet { return oltp.CapSet("") }, nil)
	if err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithToken(ops.WithUser(context.Background(), "fbot"), ops.TokenAuth{ID: "T",
		Caps: func(string) ops.Caps { return oltp.CapSet("") }, ForgeAgent: func() bool { return true }})
	call := func(body, ask string) (int, string) {
		req := httptest.NewRequest("POST", "/api/v1/agents/grant", strings.NewReader(body)).WithContext(ctx)
		req.Header.Set("Content-Type", "application/json")
		if ask != "" {
			req.Header.Set("Quarry-Ask", ask)
		}
		rec := httptest.NewRecorder()
		h.ServeHTTP(rec, req)
		return rec.Code, rec.Body.String()
	}
	waiting := func() int {
		out, err := r.Invoke(asUser("dmestas"), "signoff.list", signOffListIn{})
		if err != nil {
			t.Fatal(err)
		}
		return len(out.(signOffListOut).Requests)
	}
	for _, body := range []string{
		`{"agent":"zoot","repo":"test","role":"developer"}`,    // not an agent
		`{"agent":"jva-bot","repo":"test","role":"developer"}`, // somebody else's
		`{"agent":"jva-bot","repo":"nope","role":"developer"}`, // no such repository
	} {
		plainCode, plain := call(body, "")
		askCode, asked := call(body, "putting a new agent to work")
		if plainCode == 202 || askCode != plainCode || asked != plain {
			t.Errorf("FAIL: asking answered differently from refusing for %s:\n  plain %d %s\n  asked %d %s", body, plainCode, plain, askCode, asked)
		}
	}
	if n := waiting(); n != 0 {
		t.Fatalf("FAIL: %d requests were recorded for asks that could not run", n)
	}
	// The owner's own agent, on a repository they reach, is recorded.
	if _, err := r.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: "dm-bot"}); err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	if c, b := call(`{"agent":"dm-bot","repo":"test","role":"developer"}`, "putting a new agent to work"); c != 202 {
		t.Errorf("FAIL: an ask that passes the preflight answered %d: %s", c, b)
	}
	if n := waiting(); n != 1 {
		t.Errorf("FAIL: %d requests waiting, want 1", n)
	}
	t.Log("ok")
}

// The preflight judges the target repository as the agent: the same
// ask passes where the agent can read the repository and is not asked
// where it cannot, even for the owner's own agent.
func TestAnAskCannotProbeARepositoryTheAgentCannotRead(t *testing.T) {
	_, db, _ := agentCoreRig(t, 5)
	if err := db.NewAgent("dm-bot", "dmestas", -1); err != nil {
		t.Fatal(err)
	}
	repos := testRepos(wikiIndex(t))
	can := true
	d := Deps{Repos: repos, Accounts: adminAdapter{acctAdapter{db}},
		CanRead: func(_ context.Context, repo string) bool { return can }}
	in := agentGrantIn{Agent: "dm-bot", Repo: "test", Role: "developer"}
	if !d.preflightAsk(asUser("fbot"), "agent.grant", "dmestas", in) {
		t.Fatal("FAIL: the control ask did not pass the preflight")
	}
	can = false
	if d.preflightAsk(asUser("fbot"), "agent.grant", "dmestas", in) {
		t.Error("FAIL: an ask on a repository the agent cannot read passed the preflight")
	}
	t.Log("ok")
}

// The forge's own agent may read agent.list on its token, and nothing
// else about agents; no other token may.
func TestOnlyAForgeAgentsTokenListsAgents(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	// The migration shape: a site admin re-marked as a forge agent,
	// keeping the flag.
	if err := db.AddAccount("fbot", "a-long-enough-password", true); err != nil {
		t.Fatal(err)
	}
	if err := db.SetForgeAgent("fbot", "dmestas"); err != nil {
		t.Fatal(err)
	}
	if err := db.NewDedicatedAgent("suit-bot", "repo:test", "dmestas"); err != nil {
		t.Fatal(err)
	}
	if err := db.NewAgent("jva-bot", "jva", -1); err != nil {
		t.Fatal(err)
	}
	tok := func(who string, site bool) context.Context {
		return ops.WithToken(asUser(who), ops.TokenAuth{ID: "T",
			Caps: func(string) ops.Caps { return oltp.CapSet("") },
			Site: func() bool { return site }})
	}
	out, err := r.Invoke(tok("fbot", true), "agent.list", agentListIn{Operator: "repo:test"})
	if err != nil {
		t.Fatalf("FAIL: the forge agent's token could not list: %v", err)
	}
	l := out.(agentListOut)
	if l.Count != 1 || l.Agents[0].Name != "suit-bot" {
		t.Errorf("FAIL: the list: %+v", l)
	}
	if _, err := r.Invoke(tok("fbot", true), "agent.list", agentListIn{Agent: "suit-bot"}); err != nil {
		t.Errorf("FAIL: looking an agent up by name: %v", err)
	}
	for _, who := range []string{"dmestas", "jva", "jva-bot"} {
		if _, err := r.Invoke(tok(who, who == "dmestas"), "agent.list", agentListIn{}); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: %s's token listed agents: %v", who, err)
		}
	}
	// Every other agent op still refuses the forge agent's token.
	if _, err := r.Invoke(tok("fbot", true), "agent.delete", agentNameIn{Name: "suit-bot"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: the forge agent's token deleted an agent: %v", err)
	}
	if _, err := r.Invoke(tok("fbot", true), "agent.grant", agentGrantIn{Agent: "suit-bot", Repo: "test", Role: "developer"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: the forge agent's token granted: %v", err)
	}
	// No token value anywhere in a row.
	if _, _, err := db.MintToken("suit-bot", oltp.TokenSpec{Name: "ci"}); err == nil {
		out, _ := r.Invoke(tok("fbot", true), "agent.list", agentListIn{Operator: "repo:test"})
		for _, a := range out.(agentListOut).Agents {
			for _, tk := range a.Tokens {
				if tk.Name == "" {
					t.Errorf("FAIL: a token row without a name: %+v", tk)
				}
			}
		}
	}
	t.Log("ok")
}

// A forge token that reaches only some repositories lists no agent of
// a scope it does not reach: not by name, not as a row with a blank,
// and not with the names of repositories in its tokens. An operator or
// scope it does not reach is not found; a token limited at dispatch is
// refused outright.
func TestAForgeTokenSeesOnlyTheScopesItReaches(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if err := db.AddRepo("other", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	if err := db.AddAccount("fbot", "a-long-enough-password", true); err != nil {
		t.Fatal(err)
	}
	if err := db.SetForgeAgent("fbot", "dmestas"); err != nil {
		t.Fatal(err)
	}
	for _, a := range [][2]string{{"suit-bot", "repo:test"}, {"o-bot", "repo:other"}} {
		if err := db.NewDedicatedAgent(a[0], a[1], "dmestas"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.NewScopedAgent("hid-bot", "jva", "repo:other", -1); err != nil {
		t.Fatal(err)
	}
	if err := db.NewScopedAgent("vis-bot", "jva", "repo:test", -1); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.MintToken("suit-bot", oltp.TokenSpec{Name: "ci", Scopes: []string{"sync"}, Repos: []string{"test", "other"}}); err != nil {
		t.Logf("note: a two-repository token was not minted (%v); the token-repos leg is skipped", err)
	}
	reach := func(repo string) bool { return repo == "test" }
	tok := ops.WithToken(asUser("fbot"), ops.TokenAuth{ID: "T", Reaches: reach,
		Caps: func(string) ops.Caps { return oltp.CapSet("") }, Site: func() bool { return true }})

	if _, err := r.Invoke(tok, "agent.list", agentListIn{Operator: "repo:other"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a scope the token does not reach was listed: %v", err)
	}
	if _, err := r.Invoke(tok, "agent.list", agentListIn{Agent: "o-bot"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: an agent of a scope the token does not reach was found by name: %v", err)
	}
	out, err := r.Invoke(tok, "agent.list", agentListIn{Operator: "repo:test"})
	if err != nil || out.(agentListOut).Count != 1 {
		t.Fatalf("FAIL: the reached scope: %v %+v", err, out)
	}
	for _, tk := range out.(agentListOut).Agents[0].Tokens {
		for _, repo := range tk.Repos {
			if repo != "test" {
				t.Errorf("FAIL: a token row names %q, which the caller's token does not reach", repo)
			}
		}
	}
	out, err = r.Invoke(tok, "agent.list", agentListIn{Operator: "jva"})
	if err != nil {
		t.Fatal(err)
	}
	for _, a := range out.(agentListOut).Agents {
		if a.Name == "hid-bot" || a.Scope == "repo:other" || a.Operator == "repo:other" {
			t.Errorf("FAIL: an agent of an unreached scope was listed: %+v", a)
		}
	}
	if out.(agentListOut).Count != 1 {
		t.Errorf("FAIL: %d agents listed for jva, want only vis-bot", out.(agentListOut).Count)
	}
	// An unlimited forge token sees everything.
	all := ops.WithToken(asUser("fbot"), ops.TokenAuth{ID: "T",
		Caps: func(string) ops.Caps { return oltp.CapSet("") }, Site: func() bool { return true }})
	if out, err := r.Invoke(all, "agent.list", agentListIn{Operator: "repo:other"}); err != nil || out.(agentListOut).Count != 1 {
		t.Errorf("FAIL: an unlimited forge token lost sight of a scope: %v", err)
	}
	// Limited at dispatch: refused before the op runs.
	lim := ops.WithToken(asUser("fbot"), ops.TokenAuth{ID: "T", Limited: true, Reaches: reach,
		Caps: func(string) ops.Caps { return oltp.CapSet("") }, Site: func() bool { return true }})
	if _, err := r.Invoke(lim, "agent.list", agentListIn{}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a limited token listed agents: %v", err)
	}
	t.Log("ok")
}