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")
}