Skip to content
quarry Artifact [57608830f0]
Login

Artifact [57608830f0]

Artifact 57608830f0785cc8cbdf18933b93daf838cc19076fa29d6dfaef9c188ca149e0:


package api

import (
	"errors"
	"path/filepath"
	"strings"
	"testing"

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

// agentCoreRig is agentRig with an ownership registry, the repository
// "test" the ops resolve, and the agent cap set.
func agentCoreRig(t *testing.T, perPerson int) (*ops.Registry, *oltp.DB, *fakeOwners) {
	t.Helper()
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	for _, who := range []string{"dmestas", "jva", "mallory"} {
		if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddRepo("test", t.TempDir(), "", oltp.Public); err != nil {
		t.Fatal(err)
	}
	owners := newFakeOwners()
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)),
		Teams: db, Accounts: adminAdapter{acctAdapter{db}}, Owners: owners, Categories: db, Roster: dbRoster{db}, AgentsPerPerson: perPerson,
		CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
		t.Fatal(err)
	}
	return r, db, owners
}

func TestAgentNewListDeleteAndTheCap(t *testing.T) {
	r, db, _ := agentCoreRig(t, 2)
	for _, name := range []string{"jva-coder", "jva-release"} {
		if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: name}); err != nil {
			t.Fatalf("FAIL: agent.new %s: %v", name, err)
		}
	}
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-third"}); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: the third agent over a cap of two: %v", err)
	} else {
		t.Log("ok: agents_per_person caps creation")
	}
	if db.Operator("jva-coder") != "jva" {
		t.Error("FAIL: the caller did not become the operator")
	}
	if a, _ := db.AccountByName("jva-coder"); a.Admin || a.Pending {
		t.Errorf("FAIL: a new agent reads admin=%v pending=%v", a.Admin, a.Pending)
	}
	if _, ok := db.Authenticate("jva-coder", ""); ok {
		t.Error("FAIL: an agent signed in with an empty password")
	}
	// Only a site admin creates for somebody else; the admin is never capped.
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "x", Operator: "mallory"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a person created an agent for somebody else: %v", err)
	}
	for _, name := range []string{"bot1", "bot2", "bot3"} {
		if _, err := r.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: name}); err != nil {
			t.Errorf("FAIL: the site admin was capped: %v", err)
		}
	}
	if _, err := r.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: "mallory-bot", Operator: "mallory"}); err != nil {
		t.Errorf("FAIL: the site admin could not create for somebody else: %v", err)
	}
	// Listing: your own; a site admin may ask for anybody's.
	out, err := r.Invoke(asUser("jva"), "agent.list", agentListIn{})
	if err != nil {
		t.Fatal(err)
	}
	if got := out.(agentListOut); got.Count != 2 || got.Agents[0].Name != "jva-coder" {
		t.Errorf("FAIL: jva's list: %+v", got)
	}
	if _, err := r.Invoke(asUser("jva"), "agent.list", agentListIn{Operator: "dmestas"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a person listed somebody else's agents: %v", err)
	}
	out, err = r.Invoke(asUser("dmestas"), "agent.list", agentListIn{Operator: "jva"})
	if err != nil || out.(agentListOut).Count != 2 {
		t.Errorf("FAIL: the admin's list of jva's: %v %+v", err, out)
	}
	// Deleting: never a person, never somebody else's.
	if _, err := r.Invoke(asUser("jva"), "agent.delete", agentNameIn{Name: "mallory"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: agent.delete reached a person: %v", err)
	}
	if _, err := r.Invoke(asUser("mallory"), "agent.delete", agentNameIn{Name: "jva-coder"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a stranger deleted somebody's agent: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "agent.delete", agentNameIn{Name: "jva-coder"}); err != nil {
		t.Errorf("FAIL: the operator could not delete their agent: %v", err)
	}
	if _, ok := db.AccountByName("jva-coder"); ok {
		t.Error("FAIL: the deleted agent still exists")
	} else {
		t.Log("ok: new, list and delete, each held to the operator")
	}
	// A disabled forge: zero leaves creation to site admins.
	r0, _, _ := agentCoreRig(t, 0)
	if _, err := r0.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: agents_per_person=0 let a person create: %v", err)
	}
	if _, err := r0.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: "bot"}); err != nil {
		t.Errorf("FAIL: agents_per_person=0 stopped the site admin: %v", err)
	}
}

func TestAgentTokensMintListRevokeAndRevokeAll(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	for _, name := range []string{"jva-coder", "jva-release"} {
		if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: name}); err != nil {
			t.Fatal(err)
		}
	}
	out, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: "jva-coder", Name: "session 1", Scopes: "api,sync", Repos: "test", Ceiling: "uv", Days: 30})
	if err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	minted := out.(agentTokenNewOut)
	if !strings.HasPrefix(minted.Value, "qat_") || minted.Token.Ceiling != "uv" || len(minted.Token.Repos) != 1 {
		t.Errorf("FAIL: minted %+v", minted)
	}
	if tok, ok := db.ResolveToken("jva-coder", minted.Value, oltp.ScopeAPI); !ok || tok.ID != minted.Token.ID {
		t.Error("FAIL: the minted value does not resolve")
	}
	if _, err := r.Invoke(asUser("mallory"), "agent.token.new", agentTokenNewIn{Agent: "jva-coder", Name: "x", Scopes: "api"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a stranger minted for somebody's agent: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: "mallory", Name: "x", Scopes: "api"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a token was minted for a person through the agent door: %v", err)
	}
	// On a token, none of this is reachable.
	onToken := ops.WithToken(asUser("jva"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
	for op, in := range map[string]any{
		"agent.new": agentNewIn{Name: "x"}, "agent.list": agentListIn{}, "agent.delete": agentNameIn{Name: "jva-coder"},
		"agent.token.new":  agentTokenNewIn{Agent: "jva-coder", Name: "x", Scopes: "api"},
		"agent.token.list": agentTokenListIn{Agent: "jva-coder"}, "agent.token.revoke": agentTokenRevokeIn{Agent: "jva-coder", ID: minted.Token.ID},
		"agent.token.revoke_all": struct{}{}, "agent.grant": agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "reader"},
		"account.agent": accountAgentIn{Name: "jva-coder"},
	} {
		if _, err := r.Invoke(onToken, op, in); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: %s on a token: %v", op, err)
		}
	}
	list, err := r.Invoke(asUser("jva"), "agent.token.list", agentTokenListIn{Agent: "jva-coder"})
	if err != nil || len(list.(agentTokenListOut).Tokens) != 1 {
		t.Errorf("FAIL: list: %v %+v", err, list)
	}
	if _, err := r.Invoke(asUser("jva"), "agent.token.revoke", agentTokenRevokeIn{Agent: "jva-coder", ID: minted.Token.ID}); err != nil {
		t.Errorf("FAIL: revoke: %v", err)
	}
	if _, ok := db.ResolveToken("jva-coder", minted.Value, oltp.ScopeAPI); ok {
		t.Error("FAIL: a revoked token still resolves")
	}
	var values []string
	for _, agent := range []string{"jva-coder", "jva-release"} {
		out, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: agent, Name: "s", Scopes: "api"})
		if err != nil {
			t.Fatal(err)
		}
		values = append(values, out.(agentTokenNewOut).Value)
	}
	out, err = r.Invoke(asUser("jva"), "agent.token.revoke_all", struct{}{})
	if err != nil || out.(agentRevokeAllOut).Revoked != 2 {
		t.Fatalf("FAIL: revoke_all: %v %+v", err, out)
	}
	for i, agent := range []string{"jva-coder", "jva-release"} {
		if _, ok := db.ResolveToken(agent, values[i], oltp.ScopeAPI); ok {
			t.Errorf("FAIL: %s's token survived revoke_all", agent)
		}
	}
	t.Log("ok: mint, list, revoke and revoke_all, each the operator's and never a token's")
}

func TestAgentGrantClipsAndRefusesBeyondReach(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
		t.Fatal(err)
	}
	if err := db.Grant("jva", "test", oltp.Read); err != nil {
		t.Fatal(err)
	}
	out, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "developer"})
	if err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	got := out.(agentGrantOut)
	if !got.Clipped || got.Warning == "" || oltp.CapSet(got.Caps).HasAny("i") {
		t.Errorf("FAIL: developer on a reader's agent stored as %+v", got)
	} else {
		t.Log("ok: the string is clipped to the operator's letters and the response says so")
	}
	if db.EffectiveCaps("jva-coder", "test").HasAny("i") {
		t.Error("FAIL: the agent checks in where its operator cannot")
	}
	if _, err := r.Invoke(asUser("mallory"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "reader"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a stranger granted somebody's agent: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "nope", Role: "reader"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a grant on a repository that is not served: %v", err)
	}
	// account.grant by a repository admin on the agent still resolves
	// under the operator's letters.
	if err := db.Grant("dmestas", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	if _, err := r.Invoke(asUser("dmestas"), "account.grant", accountGrantIn{Name: "jva-coder", Repo: "test", Role: "admin"}); err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	if eff := db.EffectiveCaps("jva-coder", "test"); eff.HasAny("a") || eff.HasAny("i") {
		t.Error("FAIL: a repository admin's grant lifted the agent above its operator")
	} else {
		t.Log("ok: account.grant on an agent resolves under the operator's letters")
	}
}

// The deed: the three deed acts refuse an agent before the deed is
// consulted, held or unclaimed alike.
func TestDeedActsRefuseAnAgent(t *testing.T) {
	r, db, owners := agentCoreRig(t, 5)
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"jva", "jva-coder"} {
		if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
	}
	d := Deps{Owners: owners, Accounts: acctAdapter{db}}
	for _, held := range []string{"", "jva"} {
		owners.owner["test"] = held
		for _, verb := range []string{"destroy", "stop serving"} {
			err := d.refuseUnlessTheirs(asUser("jva-coder"), "test", verb)
			if !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "jva-coder") {
				t.Errorf("FAIL: %s by an agent, deed %q: %v", verb, held, err)
			}
		}
		if _, err := r.Invoke(asUser("jva-coder"), "repo.claim", repoClaimIn{Repo: "test"}); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: claim by an agent, deed %q: %v", held, err)
		}
		if _, err := r.Invoke(asUser("jva-coder"), "repo.claim", repoClaimIn{Repo: "test", Account: "jva"}); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: an agent handed a deed on, deed %q: %v", held, err)
		}
	}
	if owners.owner["test"] != "jva" {
		t.Error("FAIL: the deed moved")
	}
	// A person with the same letters still may.
	owners.owner["test"] = ""
	if err := d.refuseUnlessTheirs(asUser("jva"), "test", "destroy"); err != nil {
		t.Errorf("FAIL: a person refused on an unclaimed repository: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "repo.claim", repoClaimIn{Repo: "test"}); err != nil {
		t.Errorf("FAIL: a person could not claim: %v", err)
	}
	// And nothing hands an agent a deed through the store.
	if err := db.SetOwner("test", "jva-coder"); !oltp.IsRefusal(err) {
		t.Errorf("FAIL: SetOwner took an agent: %v", err)
	} else {
		t.Log("ok: delete, remove, claim and SetOwner all refuse an agent; a person passes")
	}
}

// Marking clears the password and ends the sessions; clearing the mark
// on a passwordless account returns an invite link; invite and reset
// refuse an agent by name.
func TestMarkingClearsThePasswordAndClearingMintsAnInvite(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	sess, _, ok := db.NewSession("jva", "a-long-enough-password")
	if !ok {
		t.Fatal("no session")
	}
	if _, err := r.Invoke(asUser("dmestas"), "account.agent", accountAgentIn{Name: "jva", Operator: "dmestas"}); err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	if _, ok := db.Authenticate("jva", "a-long-enough-password"); ok {
		t.Error("FAIL: the password survived the mark")
	}
	if _, ok := db.SessionAccount(sess); ok {
		t.Error("FAIL: the session survived the mark")
	}
	if _, err := r.Invoke(asUser("dmestas"), "account.invite", accountInviteIn{Name: "jva"}); !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), "jva") {
		t.Errorf("FAIL: account.invite on an agent: %v", err)
	}
	if _, err := r.Invoke(asUser("dmestas"), "account.reset", accountResetIn{Name: "jva"}); err == nil || !strings.Contains(err.Error(), "jva") {
		t.Errorf("FAIL: account.reset on an agent: %v", err)
	}
	out, err := r.Invoke(asUser("dmestas"), "account.agent", accountAgentIn{Name: "jva"})
	if err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	link := out.(accountAgentOut).Link
	if !strings.Contains(link, "/invite/") {
		t.Fatalf("FAIL: clearing the mark returned no invite link: %+v", out)
	}
	token := link[strings.LastIndex(link, "/")+1:]
	if name, ok := db.InviteFor(token); !ok || name != "jva" {
		t.Errorf("FAIL: the link is not a live invite for jva: %q %v", name, ok)
	} else {
		t.Log("ok: the mark clears the password and the sessions; clearing it mints a live invite")
	}
}

// adminAdapter is acctAdapter with the AccountAdmin half, so the
// invite and reset doors answer.
type adminAdapter struct{ acctAdapter }

func (a adminAdapter) Sessions(name string) ([]SessionInfo, error) {
	rows, err := a.DB.Sessions(name)
	if err != nil {
		return nil, err
	}
	out := make([]SessionInfo, 0, len(rows))
	for _, s := range rows {
		out = append(out, SessionInfo{ID: s.ID, Account: s.Account})
	}
	return out, nil
}
func (a adminAdapter) EndSession(id, account string) error { return a.DB.EndSessionID(id, account) }
func (a adminAdapter) EndSessions(account string) error    { return a.DB.EndAccountSessions(account) }

// An agent does not manage teams: every team op that changes grants,
// membership or where a repository lives refuses an agent caller, and
// the same calls pass for its operator.
func TestTeamOpsRefuseAnAgent(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	yes := true
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"jva", "jva-coder"} {
		if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
	}
	if _, err := r.Invoke(asUser("jva"), "team.create", teamCreateIn{Name: "crew", Title: "Crew"}); err != nil {
		t.Fatalf("FAIL: the person could not create a team: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "team.member", teamMemberIn{Name: "crew", Account: "jva-coder", Owner: &yes}); err != nil {
		t.Fatalf("FAIL: the person could not add the agent as an owner: %v", err)
	}
	for op, in := range map[string]any{
		"team.create":             teamCreateIn{Name: "botcrew", Title: "Bots"},
		"team.delete":             teamDeleteIn{Name: "crew", Confirm: "crew"},
		"team.member":             teamMemberIn{Name: "crew", Account: "mallory", Caps: "xy"},
		"team.unmember":           teamMemberIn{Name: "crew", Account: "jva"},
		"team.repo.add":           teamRepoIn{Name: "crew", Repo: "test"},
		"team.repo.offer.accept":  teamRepoIn{Name: "crew", Repo: "test"},
		"team.repo.offer.decline": teamRepoIn{Name: "crew", Repo: "test"},
		"team.repo.remove":        teamRepoIn{Name: "crew", Repo: "test"},
	} {
		if _, err := r.Invoke(asUser("jva-coder"), op, in); !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "jva-coder") {
			t.Errorf("FAIL: %s by an agent: %v", op, err)
		}
	}
	if _, ok := db.TeamByName("botcrew"); ok {
		t.Error("FAIL: the agent's team exists")
	}
	if _, err := r.Invoke(asUser("jva"), "team.repo.add", teamRepoIn{Name: "crew", Repo: "test"}); err != nil {
		t.Errorf("FAIL: the person could not place the repository: %v", err)
	} else {
		t.Log("ok: an agent is refused by every team op that changes grants, membership or placement; its operator is not")
	}
}

// A category string hands letters to everyone in the category, and
// nobody sits outside the operator intersection: an agent with a but
// not y may not write y into any category; its operator may.
func TestCategorySetRefusesAnAgentWritingLettersItDoesNotHold(t *testing.T) {
	r, db, _ := agentCoreRig(t, 5)
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"jva", "jva-coder"} {
		if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
	}
	if db.EffectiveCaps("jva-coder", "test").HasAny("y") {
		t.Fatal("rig: the agent holds y")
	}
	for _, cat := range []string{"nobody", "reader"} {
		before, _ := db.CategoryCapsFrom("test", cat)
		if _, err := r.Invoke(asUser("jva-coder"), "repo.category.set", categorySetIn{Repo: "test", Category: cat, Caps: before + "y"}); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: the agent wrote y into %s: %v", cat, err)
		}
		if after, _ := db.CategoryCapsFrom("test", cat); strings.ContainsRune(after, 'y') {
			t.Errorf("FAIL: %s now holds y", cat)
		}
		// Letters it does hold are fine.
		if _, err := r.Invoke(asUser("jva-coder"), "repo.category.set", categorySetIn{Repo: "test", Category: cat, Caps: before}); err != nil {
			t.Errorf("FAIL: the agent could not rewrite %s with letters it holds: %v", cat, err)
		}
	}
	if db.EffectiveCaps("jva-coder", "test").HasAny("y") {
		t.Error("FAIL: the agent reached y through a category")
	}
	before, _ := db.CategoryCapsFrom("test", "nobody")
	if _, err := r.Invoke(asUser("jva"), "repo.category.set", categorySetIn{Repo: "test", Category: "nobody", Caps: before + "y"}); err != nil {
		t.Errorf("FAIL: the person was held to the agent's rule: %v", err)
	} else {
		t.Log("ok: an agent writes only letters it resolves to into a category; its operator is not held to that")
	}
}

// agent.list says whether the caller may make another agent, so a page
// draws its create control only where agent.new would accept it.
func TestAgentListSaysWhetherTheCallerMayCreate(t *testing.T) {
	r, _, _ := agentCoreRig(t, 1)
	can := func(user string) bool {
		out, err := r.Invoke(asUser(user), "agent.list", agentListIn{})
		if err != nil {
			t.Fatal(err)
		}
		return out.(agentListOut).CanCreate
	}
	if !can("jva") {
		t.Error("FAIL: under the cap, creating is not offered")
	}
	if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-one"}); err != nil {
		t.Fatal(err)
	}
	if can("jva") {
		t.Error("FAIL: at the cap, creating is offered")
	}
	if !can("dmestas") {
		t.Error("FAIL: a site admin is never capped")
	}
	off, _, _ := agentCoreRig(t, 0)
	if out, _ := off.Invoke(asUser("jva"), "agent.list", agentListIn{}); out.(agentListOut).CanCreate {
		t.Error("FAIL: creating is offered on a forge that allows none")
	}
}