package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"craftdesign.group/quarry/internal/cisecret"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// setupRig is a forge with real letters in the store: dmestas (a site
// admin, admin of suit, ctx and the private secret), jva (a reader),
// their agents, a forge agent answerable to dmestas and a repository's
// own agent.
type setupRig struct {
t *testing.T
db *oltp.DB
deps Deps
r *ops.Registry
h http.Handler
rec *settingRecorder
coll *PickupCollector
// blind makes the api-level reading gate say yes to everyone, so a
// request can reach the store with a repository the approver lost.
blind bool
// failRefs and failFinish fail the stores that hold an approved
// request's result, as a database error would.
failRefs, failFinish bool
// failRetract makes taking a setup back fail too.
failRetract bool
prov *recProvision
// attn is the attention plane the forge publishes to; attnDown makes
// publishing to it fail, as a broker outage would.
attn *fakeAttention
attnDown bool
}
// downableAttention is the rig's attention plane, which can be made to
// fail on publish.
type downableAttention struct {
*fakeAttention
rig *setupRig
}
func (a downableAttention) Attend(account string, e AttentionEvent) (uint64, error) {
if a.rig.attnDown {
return 0, errors.New("broker down")
}
return a.fakeAttention.Attend(account, e)
}
// recProvision records the notify provisioning calls a setup makes.
type recProvision struct {
mu sync.Mutex
calls []string
}
func (p *recProvision) note(c string) {
p.mu.Lock()
defer p.mu.Unlock()
p.calls = append(p.calls, c)
}
func (p *recProvision) EnsureSubscriber(_ context.Context, a string) error {
p.note("ensure " + a)
return nil
}
func (p *recProvision) RemoveSubscriber(_ context.Context, a string) error {
p.note("remove " + a)
return nil
}
func (p *recProvision) SetTopicMember(_ context.Context, repo, a string, m bool) error {
p.note(fmt.Sprintf("topic %s %s %v", repo, a, m))
return nil
}
func (p *recProvision) seen() []string {
p.mu.Lock()
defer p.mu.Unlock()
return append([]string(nil), p.calls...)
}
// retractFailing is the account store whose RetractSetup can be made to
// fail.
type retractFailing struct {
adminAdapter
rig *setupRig
}
func (a retractFailing) RetractSetup(agent, tokenID string) error {
if a.rig.failRetract {
return errors.New("disk I/O error")
}
return a.adminAdapter.RetractSetup(agent, tokenID)
}
type setupSignOffs struct {
SignOffStore
rig *setupRig
}
func (s setupSignOffs) SetSignOffRefs(id, refs string) error {
if s.rig.failRefs {
s.rig.failRefs = false // one transient failure
return errors.New("disk I/O error")
}
return s.SignOffStore.SetSignOffRefs(id, refs)
}
func (s setupSignOffs) FinishSignOff(id, result, secret string) error {
if s.rig.failFinish {
s.rig.failFinish = false // one transient failure
return errors.New("disk I/O error")
}
return s.SignOffStore.FinishSignOff(id, result, secret)
}
func newSetupRig(t *testing.T, box ...cisecret.Box) *setupRig {
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"} {
if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
t.Fatal(err)
}
}
repos := map[string]*Repo{}
for _, name := range []string{"suit", "ctx", "secret"} {
if err := db.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
repos[name] = &Repo{Index: wikiIndex(t), Code: "code-" + name}
if err := db.Grant("dmestas", name, oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
}
if err := db.SetRepoVisibility("secret", oltp.Private); err != nil {
t.Fatal(err)
}
for agent, person := range map[string]string{"dmestas-bot": "dmestas", "jva-bot": "jva"} {
if err := db.NewAgent(agent, person, -1); err != nil {
t.Fatal(err)
}
}
if err := db.NewForgeAgent("zeitgeist", "dmestas"); err != nil {
t.Fatal(err)
}
if err := db.NewDedicatedAgent("suit-ci", "repo:suit", "dmestas"); err != nil {
t.Fatal(err)
}
g := &setupRig{t: t, db: db, rec: &settingRecorder{}, prov: &recProvision{}, attn: newFakeAttention()}
sbox := testBox(t)
if len(box) > 0 {
sbox = box[0]
}
holds := func(who, repo string) CapSet { return db.EffectiveCaps(who, repo) }
canRead := func(ctx context.Context, repo string) bool {
if g.blind {
return true
}
if tok, ok := ops.TokenOf(ctx); ok && tok.Reaches != nil && !tok.Reaches(repo) {
return false
}
return db.EffectiveCaps(ops.User(ctx), repo).HasAny("gjoh2r")
}
deps := Deps{Version: "v", SettingLog: g.rec, Provision: g.prov, SecretBox: sbox, Acts: db, Prefs: db, CapsFor: holds, CanRead: canRead,
Origin: "https://forge.test", AgentsPerPerson: 5, Attention: downableAttention{g.attn, g},
Repos: NewRepos(repos),
Accounts: retractFailing{adminAdapter{acctAdapter{db}}, g}, SignOffs: setupSignOffs{testSignOffs{db}, g}}
r := ops.NewRegistry()
if err := RegisterAll(r, deps); err != nil {
t.Fatal(err)
}
h, err := RouterWithCaps(r, "v", holds, nil)
if err != nil {
t.Fatal(err)
}
lim := &missLimiter{miss: map[string]int{}}
g.deps, g.r, g.h, g.coll = deps, r, h, NewPickupCollector(deps, lim, nil)
return g
}
// tokenCtx is a call on one of agent's tokens, limited to repos when any
// are named.
func tokenCtx(agent string, repos ...string) context.Context {
tok := ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("") }}
if len(repos) > 0 {
tok.Limited = true
tok.Reaches = func(r string) bool {
for _, have := range repos {
if have == r {
return true
}
}
return false
}
}
return ops.WithToken(ops.WithUser(context.Background(), agent), tok)
}
// post sends body to path as ctx's caller; ask adds Quarry-Ask and code
// adds Quarry-Pickup.
func (g *setupRig) post(ctx context.Context, body string, ask bool, code string) (int, string) {
g.t.Helper()
req := httptest.NewRequest("POST", "/api/v1/agents/setup", strings.NewReader(body)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
if ask {
req.Header.Set("Quarry-Ask", "suit needs a pushing agent for its CI")
}
if code != "" {
req.Header.Set("Quarry-Pickup", code)
}
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
return w.Code, w.Body.String()
}
// ask files a setup request as agent and returns its id; the request
// must have been taken.
func (g *setupRig) ask(agent, body, code string, repos ...string) string {
g.t.Helper()
status, out := g.post(tokenCtx(agent, repos...), body, true, code)
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
if err := json.Unmarshal([]byte(out), &asked); err != nil || status != 202 || asked.SignOff.ID == "" {
g.t.Fatalf("FAIL: asking: %d", status)
}
return asked.SignOff.ID
}
func (g *setupRig) decide(person, id string, approve bool) error {
g.t.Helper()
_, err := g.r.Invoke(asUser(person), "signoff.decide", signOffDecideIn{ID: id, Approve: approve})
return err
}
func (g *setupRig) get(who, id string) signOffGetOut {
g.t.Helper()
out, err := g.r.Invoke(asUser(who), "signoff.get", signOffGetIn{ID: id})
if err != nil {
g.t.Fatal(err)
}
return out.(signOffGetOut)
}
// nothingMade fails the test when anything a setup writes exists under
// name: the account, its grants, its tokens, or a retired name.
func (g *setupRig) nothingMade(name string) {
g.t.Helper()
if _, ok := g.db.AccountByName(name); ok {
g.t.Errorf("FAIL: an account row for %s exists", name)
}
if grants, _ := g.db.GrantsFor(name); len(grants) != 0 {
g.t.Errorf("FAIL: %s holds grants: %v", name, grants)
}
if toks, _ := g.db.Tokens(name); len(toks) != 0 {
g.t.Errorf("FAIL: %s holds %d tokens", name, len(toks))
}
if g.db.Tombstoned(name) {
g.t.Errorf("FAIL: the name %s was retired", name)
}
}
const pushBody = `{"name":"suit-push","kind":"push","repos":"suit"}`
// RFC 0030 step 3: an approved request yields an agent, a grant and a
// token whose scopes are sync and whose repositories are the one named;
// the value is in the secret half, never the public answer, and the
// agent's own signoff.get collects it once.
func TestAnApprovedPushSetupYieldsAnAgentAGrantAndANarrowToken(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
if _, ok := g.db.AccountByName("suit-push"); ok {
t.Fatalf("FAIL: asking made the agent")
}
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
a, ok := g.db.AccountByName("suit-push")
if !ok || a.Operator != "repo:suit" || a.Answerable != "dmestas" {
t.Fatalf("FAIL: the agent: %+v", a)
}
if stored, _ := g.db.CapsOf("suit-push", "suit"); stored != oltp.Write.Caps() {
t.Errorf("FAIL: the grant is %q", stored)
}
toks, _ := g.db.Tokens("suit-push")
if len(toks) != 1 || strings.Join(toks[0].Scopes, ",") != "sync" || strings.Join(toks[0].Repos, ",") != "suit" ||
toks[0].Ceiling != oltp.Write.Caps() || toks[0].Expires != "" || !strings.HasPrefix(toks[0].Name, "setup ") {
t.Fatalf("FAIL: the token: %+v", toks)
}
got := g.get("zeitgeist", id)
if strings.Contains(string(got.Result), "qat_") || got.Setup == nil || !got.Setup.Done {
t.Errorf("FAIL: the public half holds a value, or no setup view")
}
var sec struct{ Value string }
if err := json.Unmarshal(got.Secret, &sec); err != nil || !strings.HasPrefix(sec.Value, "qat_") {
t.Fatalf("FAIL: the asker's first read lacks the value")
}
if again := g.get("zeitgeist", id); len(again.Secret) != 0 {
t.Errorf("FAIL: the value was released twice")
}
var res agentSetupOut
if err := json.Unmarshal(got.Result, &res); err != nil || len(res.Remotes) != 1 || res.Remotes[0].ProjectCode != "code-suit" ||
res.Remotes[0].URL != "https://forge.test/r/suit" || res.Token.ID != toks[0].ID {
t.Errorf("FAIL: the answer: %v %+v", err, res)
}
if !t.Failed() {
t.Log("ok")
}
}
// A read client is the approver's own agent, api scope only, named
// repositories, the read-only ceiling, and granted reader on each; none
// of its token can push.
func TestAReadSetupIsTheApproversOwnAgentWithTheReadCeiling(t *testing.T) {
g := newSetupRig(t)
id := g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
a, _ := g.db.AccountByName("e-desktop")
if a.Operator != "dmestas" || a.Inherit || a.Scope != "" {
t.Errorf("FAIL: the client: operator=%q inherit=%v scope=%q", a.Operator, a.Inherit, a.Scope)
}
toks, _ := g.db.Tokens("e-desktop")
if len(toks) != 1 || strings.Join(toks[0].Scopes, ",") != "api" || strings.Join(toks[0].Repos, ",") != "suit,secret" ||
toks[0].Ceiling != oltp.SetupReadCeiling {
t.Fatalf("FAIL: the token: %+v", toks)
}
for _, repo := range []string{"suit", "secret"} {
if eff := g.db.EffectiveCaps("e-desktop", repo); !eff.HasAny("o") || eff.HasAny("i") || eff.HasAny("a") {
t.Errorf("FAIL: the client holds the wrong letters on %s", repo)
}
}
if tc := g.db.TokenCaps(toks[0], "suit"); tc.HasAny("i") || tc.HasAny("n") || !tc.HasAny("8") {
t.Errorf("FAIL: the token can write or cannot see CI")
}
// A client of one repository is scoped to it.
id = g.ask("dmestas-bot", `{"name":"e-one","kind":"read","repos":"suit"}`, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
if a, _ := g.db.AccountByName("e-one"); a.Scope != "repo:suit" {
t.Errorf("FAIL: a one-repository client is scoped %q", a.Scope)
}
if !t.Failed() {
t.Log("ok")
}
}
// A pushing agent belongs to the approver, scoped to the repository,
// when the approver does not administer it; the role must be theirs to
// give, and the owner's own letters bound the grant.
func TestAPushSetupBelongsToTheApproverWhenTheyAdministerNothingThere(t *testing.T) {
g := newSetupRig(t)
if err := g.db.Grant("jva", "ctx", oltp.Write); err != nil {
t.Fatal(err)
}
id := g.ask("jva-bot", `{"name":"ctx-push","kind":"push","repos":"ctx"}`, "")
if err := g.decide("jva", id, true); err != nil {
t.Fatal(err)
}
a, _ := g.db.AccountByName("ctx-push")
if a.Operator != "jva" || a.Scope != "repo:ctx" || a.Inherit {
t.Errorf("FAIL: the agent: operator=%q scope=%q inherit=%v", a.Operator, a.Scope, a.Inherit)
}
// jva holds only developer on suit's neighbour; a maintainer role
// would be cut, so it is refused when asked.
status, _ := g.post(tokenCtx("jva-bot"), `{"name":"ctx-maint","kind":"push","repos":"ctx","role":"maintainer"}`, true, "")
if status != 403 {
t.Errorf("FAIL: a role the approver cannot give was asked: %d", status)
}
g.nothingMade("ctx-maint")
// And belongs_to=repository where jva holds no admin letter is refused.
if status, _ := g.post(tokenCtx("jva-bot"), `{"name":"ctx-repo","kind":"push","repos":"ctx","belongs_to":"repository"}`, true, ""); status != 403 {
t.Errorf("FAIL: a repository agent without the admin letter: %d", status)
}
// belongs_to=person on a repository dmestas administers makes the
// agent dmestas's own.
id = g.ask("zeitgeist", `{"name":"suit-mine","kind":"push","repos":"suit","belongs_to":"person"}`, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
if a, _ := g.db.AccountByName("suit-mine"); a.Operator != "dmestas" || a.Scope != "repo:suit" {
t.Errorf("FAIL: belongs_to person: %+v", a)
}
if !t.Failed() {
t.Log("ok")
}
}
// Who may ask: an agent its person operates and the forge agent; a
// person's token, a repository's own agent and an agent without
// Quarry-Ask are refused as any credential op refuses a token.
func TestOnlyAnAgentWithAPersonMayAskForSetup(t *testing.T) {
g := newSetupRig(t)
for _, agent := range []string{"zeitgeist", "dmestas-bot"} {
if status, _ := g.post(tokenCtx(agent), `{"name":"n-`+agent+`","kind":"push","repos":"suit"}`, true, ""); status != 202 {
t.Errorf("FAIL: %s could not ask: %d", agent, status)
}
}
waiting := func() int {
rows, _ := g.db.SignOffsFor("dmestas", 50)
return len(rows)
}
before := waiting()
refused := map[string]context.Context{
"a person's token": tokenCtx("dmestas"),
"a repository's own agent": tokenCtx("suit-ci"),
"another person's agent": tokenCtx("jva-bot"),
"an agent for a stranger": tokenCtx("nobody-at-all"),
"a token limited elsewhere": tokenCtx("dmestas-bot", "ctx"),
}
for name, ctx := range refused {
if status, _ := g.post(ctx, `{"name":"x-`+strings.ReplaceAll(name, " ", "")+`","kind":"push","repos":"suit"}`, true, ""); status != 403 && status != 401 {
t.Errorf("FAIL: %s got %d, want a refusal", name, status)
}
}
if waiting() != before {
t.Errorf("FAIL: a refused asker left a request")
}
// A token limited to the repository it names may ask.
if status, _ := g.post(tokenCtx("dmestas-bot", "suit"), `{"name":"n-limited","kind":"push","repos":"suit"}`, true, ""); status != 202 {
t.Errorf("FAIL: a token limited to suit could not ask for suit: %d", status)
}
// Without Quarry-Ask, the same refusal agent.token.new gives.
st, body := g.post(tokenCtx("dmestas-bot"), pushBody, false, "")
st2, body2 := func() (int, string) {
req := httptest.NewRequest("POST", "/api/v1/agents/tokens", strings.NewReader(`{"agent":"x","name":"n","scopes":"sync"}`)).WithContext(tokenCtx("dmestas-bot"))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
return w.Code, w.Body.String()
}()
if st != st2 || st != 403 || !strings.Contains(body, "needs the account itself") || !strings.Contains(body2, "needs the account itself") {
t.Errorf("FAIL: the plain call: %d %s vs %d %s", st, body, st2, body2)
}
// And no one may run it, a session included: it is only approved.
if _, err := g.r.Invoke(asUser("dmestas"), setupOp, agentSetupIn{Name: "direct", Kind: "push", Repos: "suit"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a session ran the setup directly: %v", err)
}
g.nothingMade("direct")
if !t.Failed() {
t.Log("ok")
}
}
// Asking is judged as the owner would run it, and says no more than
// refusing: a name that exists, a repository the asker cannot read, one
// that is not there, and one the owner does not reach are each the
// plain refusal, and a repository the asker cannot read answers exactly
// as a missing one.
func TestASetupTheOwnerCouldNotRunIsRefusedWhenAsked(t *testing.T) {
g := newSetupRig(t)
count := func() int { rows, _ := g.db.SignOffsFor("jva", 50); return len(rows) }
ask := func(body string) (int, string) { return g.post(tokenCtx("jva-bot"), body, true, "") }
// The account names a person: taken.
taken, _ := ask(`{"name":"dmestas","kind":"read","repos":"suit"}`)
// A repository only dmestas reaches, and one nobody made.
hidden, hiddenBody := ask(`{"name":"e-hid","kind":"read","repos":"secret"}`)
missing, missingBody := ask(`{"name":"e-hid","kind":"read","repos":"nonesuch"}`)
if hidden != 403 || missing != 403 || taken != 403 {
t.Errorf("FAIL: asked anyway: taken=%d hidden=%d missing=%d", taken, hidden, missing)
}
if hiddenBody != missingBody {
t.Errorf("FAIL: a repository the asker cannot read answered differently from a missing one:\n%s\n%s", hiddenBody, missingBody)
}
// Read and role shape, said in words: admin is never asked for.
st, body := g.post(tokenCtx("dmestas-bot"), `{"name":"adm","kind":"push","repos":"suit","role":"admin"}`, true, "")
if st != 400 && st != 422 || !strings.Contains(body, "admin") {
t.Errorf("FAIL: admin was not refused in words: %d %s", st, body)
}
for name, body := range map[string]string{
"two pushed repositories": `{"name":"p2","kind":"push","repos":"suit,ctx"}`,
"a role on a read client": `{"name":"r1","kind":"read","repos":"suit","role":"developer"}`,
"a kind that is not one": `{"name":"k1","kind":"admin","repos":"suit"}`,
"twenty-one repositories": `{"name":"r21","kind":"read","repos":"a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2,b3,b4,b5,b6,b7,b8,b9,c0"}`,
"a repository belonging": `{"name":"rb","kind":"read","repos":"suit","belongs_to":"repository"}`,
"a role that is not there": `{"name":"rr","kind":"push","repos":"suit","role":"king"}`,
} {
if st, _ := g.post(tokenCtx("dmestas-bot"), body, true, ""); st != 400 && st != 422 {
t.Errorf("FAIL: %s was not refused as a bad request: %d", name, st)
}
}
// A name that is an agent somebody else operates: the plain refusal.
if st, _ := g.post(tokenCtx("dmestas-bot"), `{"name":"jva-bot","kind":"read","repos":"suit"}`, true, ""); st != 403 {
t.Errorf("FAIL: somebody else's agent's name was taken: %d", st)
}
// Setup makes new agents only: the owner's own agent's name, and a
// person's name in another case, are the plain refusal.
for _, name := range []string{"dmestas-bot", "Dmestas-Bot", "DMestas"} {
if st, _ := g.post(tokenCtx("dmestas-bot"), `{"name":"`+name+`","kind":"read","repos":"suit"}`, true, ""); st != 403 {
t.Errorf("FAIL: the existing account %s was taken: %d", name, st)
}
}
if count() != 0 {
t.Errorf("FAIL: a refused ask left requests")
}
g.nothingMade("e-hid")
if !t.Failed() {
t.Log("ok")
}
}
// The transaction does not start when the name was taken or a
// repository changed between asking and approving; the request reads
// failed with the reason and nothing was made.
func TestASetupWhoseNameWasTakenAfterAskingFailsWholeAndSaysSo(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
if err := g.db.AddAccount("suit-push", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approving a request whose name was taken succeeded")
}
got := g.get("dmestas", id)
if got.Request.State != "failed" || got.Request.Err == "" || got.Setup == nil || got.Setup.Done {
t.Errorf("FAIL: the request: %+v", got.Request)
}
if grants, _ := g.db.GrantsFor("suit-push"); len(grants) != 0 {
t.Errorf("FAIL: grants were written")
}
if toks, _ := g.db.Tokens("suit-push"); len(toks) != 0 {
t.Errorf("FAIL: a token was minted")
}
if !t.Failed() {
t.Log("ok")
}
}
// RFC 0030 step 3: a forced grant failure leaves no account row. The
// approver loses the second repository between asking and approving
// while the reading gate in front is blind, so the failure comes from
// the store's own grant, after the account row and the first grant were
// written.
func TestAForcedGrantFailureLeavesNoAccountRow(t *testing.T) {
g := newSetupRig(t)
id := g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
if err := g.db.Revoke("dmestas", "secret"); err != nil {
t.Fatal(err)
}
g.blind = true
err := g.decide("dmestas", id, true)
g.blind = false
if err == nil {
t.Fatalf("FAIL: the grant on a repository the approver lost succeeded")
}
g.nothingMade("e-desktop")
if got := g.get("dmestas", id); got.Request.State != "failed" {
t.Errorf("FAIL: the request is %q", got.Request.State)
}
// Nothing remains, so the same request can be asked and approved once
// the repository is reachable again.
if err := g.db.Grant("dmestas", "secret", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
id = g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Errorf("FAIL: a retry after the failure: %v", err)
}
if !t.Failed() {
t.Log("ok")
}
}
// A forge with no secret key cannot hold the value: refused before
// anything is made.
func TestASetupOnAForgeWithNoSecretKeyMakesNothing(t *testing.T) {
g := newSetupRig(t, cisecret.Box{})
id := g.ask("zeitgeist", pushBody, "")
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approved with nothing to seal with")
}
g.nothingMade("suit-push")
if !t.Failed() {
t.Log("ok")
}
}
// Failing to hold the result after the commit takes the setup back
// whole: no account row, no grant, no token, the name not retired, and
// nothing outward ever went out (no log row, receipt or topic
// membership), at each of the two writes that hold it.
func TestAResultThatCannotBeHeldTakesTheSetupBackWithNothingOutward(t *testing.T) {
for name, set := range map[string]func(*setupRig){
"the lapse references": func(g *setupRig) { g.failRefs = true },
"the sealed result": func(g *setupRig) { g.failFinish = true },
} {
t.Run(name, func(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
set(g)
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approved though its result could not be held")
}
g.failRefs, g.failFinish = false, false
g.nothingMade("suit-push")
if calls := g.prov.seen(); len(calls) != 0 {
t.Errorf("FAIL: provisioning went out: %v", calls)
}
for _, row := range g.rec.rows {
if row.field == "agent-setup" {
t.Errorf("FAIL: a settings-log row was written for a setup that was undone")
}
}
acts, _ := g.db.ForgeActs("", 100)
for _, a := range acts {
if a.Op == setupOp {
t.Errorf("FAIL: a receipt was written for a setup that was undone")
}
}
got := g.get("dmestas", id)
if got.Request.State != "failed" || got.Setup == nil || got.Setup.Outcome != setupUndone {
t.Errorf("FAIL: the page's truth: %q %+v", got.Request.State, got.Setup)
}
if !t.Failed() {
t.Log("ok")
}
})
}
}
// When even the retract fails, the request says what remains, and the
// outcome is not "nothing was made".
func TestASetupThatCannotBeTakenBackSaysWhatRemains(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
g.failFinish, g.failRetract = true, true
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approved though its result could not be held")
}
g.failFinish, g.failRetract = false, false
if _, ok := g.db.AccountByName("suit-push"); !ok {
t.Fatalf("FAIL: the test did not leave the agent behind")
}
got := g.get("dmestas", id)
if got.Request.State != "failed" || got.Setup == nil || got.Setup.Outcome != setupLeft ||
len(got.Setup.Left) != 2 || !strings.Contains(got.Setup.Left[0], "suit-push") {
t.Errorf("FAIL: what remains is not told: %q %+v", got.Request.State, got.Setup)
}
if !t.Failed() {
t.Log("ok")
}
}
// A setup refused inside the transaction (the name was taken since
// asking) says nothing was made, as an outcome on the request.
func TestARefusedSetupRecordsThatNothingWasMade(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
if err := g.db.AddAccount("Suit-Push", "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approved over a name taken in another case")
}
if v := g.get("dmestas", id).Setup; v == nil || v.Outcome != setupUndone {
t.Errorf("FAIL: the outcome: %+v", v)
}
if _, ok := g.db.AccountByName("suit-push"); ok {
t.Errorf("FAIL: the agent was made")
}
if !t.Failed() {
t.Log("ok")
}
}
// What the owner is shown is decided once, when the request is filed,
// and stored in it: who the agent belongs to and the ceiling. If the
// approver can no longer make that choice, approval refuses rather than
// falling back to the other.
func TestASetupRunsWhatWasStoredAtAskTime(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
row, _, _ := g.db.SignOffByID(id)
var in agentSetupIn
if err := json.Unmarshal([]byte(row.Input), &in); err != nil || in.BelongsTo != "repository" || in.Ceiling != oltp.Write.Caps() || in.Role != "developer" {
t.Fatalf("FAIL: the stored request: %v %+v", err, in)
}
if v := g.get("dmestas", id).Setup; v == nil || v.Belongs != "repository" || v.Owner != "suit" || v.Ceiling != oltp.Write.Caps() {
t.Errorf("FAIL: the page does not show the stored values: %+v", v)
}
// The approver loses the admin letter on suit before approving.
if err := g.db.Grant("dmestas", "suit", oltp.Write); err != nil {
t.Fatal(err)
}
if v := g.get("dmestas", id).Setup; v == nil || v.Belongs != "repository" || v.Problem == "" {
t.Errorf("FAIL: the page changed what it says, or hides that the request cannot run: %+v", v)
}
if err := g.decide("dmestas", id, true); err == nil {
t.Fatalf("FAIL: approved a repository agent without the admin letter")
}
g.nothingMade("suit-push")
// An asker cannot choose the ceiling.
if st, _ := g.post(tokenCtx("zeitgeist"), `{"name":"x-wide","kind":"push","repos":"ctx","ceiling":"a"}`, true, ""); st != 400 && st != 422 {
t.Errorf("FAIL: a chosen ceiling was taken: %d", st)
}
// A request whose stored ceiling no longer matches the role is refused.
if _, err := g.deps.planSetup("dmestas", agentSetupIn{Name: "x-stale", Kind: "push", Repos: "ctx", Role: "developer", BelongsTo: "repository", Ceiling: "a"}); err == nil {
t.Errorf("FAIL: a stale ceiling was run")
}
if !t.Failed() {
t.Log("ok")
}
}
// Each allowed push role is shown with its letters and a sentence, and
// mints exactly those letters.
func TestEachAllowedPushRoleIsShownAndMintedWithItsLetters(t *testing.T) {
g := newSetupRig(t)
// Admin does not carry y, so publishing is its own grant (RFC 0021);
// an approver without it is refused when asking for a publisher.
if st, _ := g.post(tokenCtx("zeitgeist"), `{"name":"r-nope","kind":"push","repos":"suit","role":"publisher"}`, true, ""); st != 403 {
t.Errorf("FAIL: a publisher was asked for by an approver without y: %d", st)
}
if err := g.db.SetCaps("dmestas", "suit", "ay"); err != nil {
t.Fatal(err)
}
// Each role has a sentence of its own; what it says is not pinned.
sentences := map[string]string{}
for role, preset := range map[string]oltp.Role{"developer": oltp.Write, "publisher": oltp.Publisher, "maintainer": oltp.Maintain} {
name := "r-" + role
id := g.ask("zeitgeist", `{"name":"`+name+`","kind":"push","repos":"suit","role":"`+role+`"}`, "")
v := g.get("dmestas", id).Setup
if v == nil || v.Role != role || v.Ceiling != preset.Caps() || v.RoleSentence == "" || v.RoleSentence == setupReadSent {
t.Errorf("FAIL: the view of %s: %+v", role, v)
continue
}
if sentences[v.RoleSentence] != "" {
t.Errorf("FAIL: %s is described by the sentence %s has", role, sentences[v.RoleSentence])
}
sentences[v.RoleSentence] = role
if err := g.decide("dmestas", id, true); err != nil {
t.Errorf("FAIL: approving %s: %v", role, err)
continue
}
if toks, _ := g.db.Tokens(name); len(toks) != 1 || toks[0].Ceiling != preset.Caps() {
t.Errorf("FAIL: the token of %s: %+v", role, toks)
}
}
if !t.Failed() {
t.Log("ok")
}
}
// RFC-level hole: an op with a preflight is judged at ask time on a
// plan step as on its own call, as the asker. Each preflight has a
// refused input listed here; one without an entry fails by name. And
// agent.setup is not a plan step at all.
func TestEveryPreflightIsEnforcedForAPlanStepToo(t *testing.T) {
g := newSetupRig(t)
refused := map[string]string{
// jva-bot cannot read secret, and jva-bot is nobody's grantee there.
"agent.grant": `{"agent":"jva-bot","repo":"secret","role":"reader"}`,
}
filed := func() int { rows, _ := g.db.SignOffsFor("jva", 50); return len(rows) }
plan := func(agent, op, input string) (int, string) {
body := `{"title":"t","steps":[{"op":"` + op + `","input":` + input + `}]}`
req := httptest.NewRequest("POST", "/api/v1/requests", strings.NewReader(body)).WithContext(tokenCtx(agent))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
return w.Code, w.Body.String()
}
for op := range preflights {
if op == setupOp {
continue
}
input, ok := refused[op]
if !ok {
t.Errorf("FAIL: %s has a preflight but this test has no refused input for it", op)
continue
}
// The same call asked on its own is the plain refusal.
direct := httptest.NewRequest("POST", "/api/v1/agents/grant", strings.NewReader(input)).WithContext(tokenCtx("jva-bot"))
direct.Header.Set("Content-Type", "application/json")
direct.Header.Set("Quarry-Ask", "x")
w := httptest.NewRecorder()
g.h.ServeHTTP(w, direct)
if w.Code == 202 {
t.Errorf("FAIL: %s asked on its own was filed", op)
}
if st, body := plan("jva-bot", op, input); st == 202 || filed() != 0 {
t.Errorf("FAIL: %s was filed as a plan step: %d %s", op, st, body)
}
}
// The reviewer's probe: a scoped agent plans a setup on a private
// repository it cannot read, and a setup with role admin.
for name, input := range map[string]string{
"a private repository": `{"name":"spy","kind":"push","repos":"secret","role":"maintainer"}`,
"role admin": `{"name":"adm","kind":"push","repos":"suit","role":"admin"}`,
"a reachable one": `{"name":"fine","kind":"push","repos":"suit"}`,
} {
if st, _ := plan("dmestas-bot", setupOp, input); st != 400 && st != 403 {
t.Errorf("FAIL: setup as a plan step (%s) answered %d", name, st)
}
}
rows, _ := g.db.SignOffsFor("dmestas", 50)
if len(rows) != 0 {
t.Errorf("FAIL: a plan with a setup step was filed")
}
g.nothingMade("spy")
if !t.Failed() {
t.Log("ok")
}
}
// Bound to a pickup code, the secret is collected once by the key and
// never returned by signoff.get, to the asker or the owner.
func TestASetupBoundToAPickupCodeIsCollectedOnceByTheKey(t *testing.T) {
g := newSetupRig(t)
key, code := newPickupKey(t)
id := g.ask("zeitgeist", pushBody, code)
if st, _ := g.collectNow(key); st != 202 {
t.Errorf("FAIL: collecting before approval: %d", st)
}
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
for _, who := range []string{"zeitgeist", "dmestas"} {
if got := g.get(who, id); len(got.Secret) != 0 {
t.Errorf("FAIL: signoff.get gave %s the secret of a pickup-bound request", who)
}
}
st, body := g.collectNow(key)
var got struct {
Op string
Result agentSetupOut
Secret struct{ Value string }
}
if err := json.Unmarshal([]byte(body), &got); st != 200 || err != nil || got.Op != setupOp || !strings.HasPrefix(got.Secret.Value, "qat_") || got.Result.Agent != "suit-push" {
t.Fatalf("FAIL: collecting: %d %v", st, err)
}
// The collected value is the token's own.
if tok, ok := g.db.ResolveToken("suit-push", got.Secret.Value, "sync"); !ok || tok.Account != "suit-push" {
t.Errorf("FAIL: the collected value is not the minted token")
}
if st, _ := g.collectNow(key); st != 410 {
t.Errorf("FAIL: a second collect answered %d", st)
}
if view := g.get("dmestas", id).Setup; view == nil || view.Pickup != code {
t.Errorf("FAIL: the page's collector is not the pickup code")
}
if !t.Failed() {
t.Log("ok")
}
}
func (g *setupRig) collectNow(key string) (int, string) {
req := httptest.NewRequest("POST", "/api/v1/pickups/collect", nil)
req.Header.Set("Authorization", "Pickup "+key)
req.RemoteAddr = "192.0.2.9:4000"
w := httptest.NewRecorder()
g.coll.ServeHTTP(w, req)
return w.Code, w.Body.String()
}
// Uncollected, the token is revoked after 24 hours, the agent stays, and
// the receipt says so exactly once; a second sweep says nothing.
func TestAnUncollectedSetupRevokesTheTokenAndKeepsTheAgent(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
row, _, _ := g.db.SignOffByID(id)
if row.Pickup != "" {
t.Fatalf("FAIL: an unbound request holds a pickup code")
}
receipts := func() (n int, last string) {
acts, _ := g.db.ForgeActs("", 100)
for _, a := range acts {
if a.Op == "agent.token.revoke" && a.Owner == "suit-push" {
n++
last = a.Actor
}
}
return n, last
}
if n, _ := receipts(); n != 0 {
t.Fatalf("FAIL: a revoke before the lapse")
}
if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 {
t.Fatalf("FAIL: the sweep: %d %v", n, err)
}
toks, _ := g.db.Tokens("suit-push")
if len(toks) != 1 || toks[0].Revoked == "" {
t.Errorf("FAIL: the token was not revoked: %+v", toks)
}
if _, ok := g.db.AccountByName("suit-push"); !ok {
t.Errorf("FAIL: the agent was deleted")
}
if stored, _ := g.db.CapsOf("suit-push", "suit"); stored == "" {
t.Errorf("FAIL: the grant was removed")
}
if n, last := receipts(); n != 1 || !strings.Contains(last, "suit-push stays") || !strings.Contains(last, id) {
t.Errorf("FAIL: the lapse receipt: %d %q", n, last)
}
if _, err := g.deps.LapseSignOffSecrets(time.Now().Add(26 * time.Hour)); err != nil {
t.Fatal(err)
}
if n, _ := receipts(); n != 1 {
t.Errorf("FAIL: a second sweep wrote a second row: %d", n)
}
if got := g.get("zeitgeist", id); len(got.Secret) != 0 || got.Note == "" {
t.Errorf("FAIL: a lapsed value was still released, or no note")
}
if !t.Failed() {
t.Log("ok")
}
}
// The settings log and the agent's receipt record the creation, the
// grant and the mint with the approver as actor and the asker and the
// request named; never the value.
func TestASetupIsLoggedWithTheApproverAsActorAndNoValue(t *testing.T) {
g := newSetupRig(t)
id := g.ask("zeitgeist", pushBody, "")
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
var rows []logRow
for _, row := range g.rec.rows {
if row.field == "agent-setup" {
rows = append(rows, row)
}
}
if len(rows) != 1 || rows[0].repo != "suit" || rows[0].actor != "dmestas" ||
!strings.Contains(rows[0].now, "zeitgeist") || !strings.Contains(rows[0].now, id) || !strings.Contains(rows[0].now, "suit-push") {
t.Fatalf("FAIL: the settings log: %+v", rows)
}
if strings.Contains(rows[0].now, "qat_") {
t.Errorf("FAIL: the log row holds a value")
}
var receipts []oltp.ForgeAct
acts, _ := g.db.ForgeActs("", 100)
for _, a := range acts {
if a.Op == setupOp && a.Owner == "suit-push" {
receipts = append(receipts, a)
}
}
if len(receipts) != 1 || !strings.Contains(receipts[0].Actor, "dmestas") || !strings.Contains(receipts[0].Actor, "zeitgeist") ||
!strings.Contains(receipts[0].Actor, id) || strings.Contains(receipts[0].Actor, "qat_") {
t.Errorf("FAIL: the agent's receipt: %+v", receipts)
}
if !t.Failed() {
t.Log("ok")
}
}
// RFC 0030 step 4's data: the view carries each line of the request
// from the stored input, before and after approval.
func TestTheSetupViewCarriesEachLineOfTheStoredRequest(t *testing.T) {
g := newSetupRig(t)
_, code := newPickupKey(t)
id := g.ask("zeitgeist", `{"name":"suit-push","kind":"push","repos":"suit","role":"maintainer","days":30}`, code)
v := g.get("dmestas", id).Setup
if v == nil || v.Kind != "push" || strings.Join(v.Scopes, ",") != "sync" || strings.Join(v.Repos, ",") != "suit" ||
strings.Join(v.TokenRepos, ",") != "suit" || v.Ceiling != oltp.Maintain.Caps() || v.Days != 30 || v.Pickup != code ||
v.Role != "maintainer" || v.Belongs != "repository" || v.Owner != "suit" || v.Done || v.Problem != "" {
t.Fatalf("FAIL: the view before approval: %+v", v)
}
if err := g.decide("dmestas", id, true); err != nil {
t.Fatal(err)
}
if v = g.get("dmestas", id).Setup; v == nil || !v.Done || v.Owner != "suit" || strings.Join(v.Scopes, ",") != "sync" || v.Token == "" {
t.Errorf("FAIL: the view after approval: %+v", v)
}
// A read client for two repositories.
id = g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,ctx"}`, "")
v = g.get("dmestas", id).Setup
if v == nil || strings.Join(v.Scopes, ",") != "api" || strings.Join(v.TokenRepos, ",") != "suit,ctx" || v.Ceiling != oltp.SetupReadCeiling ||
v.Belongs != "person" || v.Owner != "dmestas" || v.Pickup != "" {
t.Errorf("FAIL: the read view: %+v", v)
}
// A request whose approver now lacks a repository says so, to the
// approver alone.
id = g.ask("dmestas-bot", `{"name":"e-two","kind":"read","repos":"secret"}`, "")
if err := g.db.Revoke("dmestas", "secret"); err != nil {
t.Fatal(err)
}
if v := g.get("dmestas", id).Setup; v == nil || v.Problem == "" {
t.Errorf("FAIL: the approver is not told the request cannot run")
}
if v := g.get("dmestas-bot", id).Setup; v == nil || v.Problem != "" {
t.Errorf("FAIL: the asker is told why the request cannot run")
}
if !t.Failed() {
t.Log("ok")
}
}
// agent.setup is parkable in principle (a lapse revokes its token) but
// can never be parked: it only runs when approved, so a person's
// session sending a pickup code with it is refused, binds no code and
// makes nothing; the code stays free for a real ask.
func TestASetupRunDirectlyWithAPickupCodeIsRefusedAndParksNothing(t *testing.T) {
g := newSetupRig(t)
d, _ := g.r.Lookup(setupOp)
if pickupAllowedFor(d) != nil {
t.Fatalf("FAIL: agent.setup is not parkable")
}
_, code := newPickupKey(t)
req := httptest.NewRequest("POST", "/api/v1/agents/setup", strings.NewReader(pushBody)).WithContext(asUser("dmestas"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Pickup", code)
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
if w.Code != 403 && w.Code != 400 {
t.Errorf("FAIL: a session's direct run with a pickup code answered %d", w.Code)
}
if _, found, _ := g.db.SignOffByPickup(code); found {
t.Errorf("FAIL: the code was bound")
}
g.nothingMade("suit-push")
if id := g.ask("zeitgeist", pushBody, code); id == "" {
t.Errorf("FAIL: the code was not free for a real ask")
}
if !t.Failed() {
t.Log("ok")
}
}