Skip to content
quarry Artifact [33d0afaf7f]
Login

Artifact [33d0afaf7f]

Artifact 33d0afaf7fe1f81256b991163202db73d80c9ad10bfce2691e299f7e6d2b68fb:


package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// credRig is a forge with a person (jva), their agent (jva-bot), a
// forge agent (fbot, answerable to dmestas) and the stores whose
// credentials an approved ask mints.
type credRig struct {
	t    *testing.T
	db   *oltp.DB
	deps Deps
	r    *ops.Registry
	h    http.Handler
	rec  *settingRecorder
	// failRevoke and failClear make the next sweep's revoke or clear
	// fail, as a database error would.
	failRevoke, failClear bool
}

type flakyExternal struct {
	ExternalCI
	rig *credRig
}

func (f flakyExternal) RevokeReporterToken(repo, hash string) error {
	if f.rig.failRevoke {
		return errors.New("disk I/O error")
	}
	return f.ExternalCI.RevokeReporterToken(repo, hash)
}

type flakySignOffs struct {
	SignOffStore
	rig *credRig
}

func (f flakySignOffs) ClearSignOffLapse(id string) error {
	if f.rig.failClear {
		return errors.New("disk I/O error")
	}
	return f.SignOffStore.ClearSignOffLapse(id)
}

func newCredRig(t *testing.T) *credRig {
	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)
		}
	}
	if err := db.AddAccount("jva-bot", "a-long-enough-password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetOperator("jva-bot", "jva"); err != nil {
		t.Fatal(err)
	}
	if err := db.NewForgeAgent("fbot", "dmestas"); err != nil {
		t.Fatal(err)
	}
	rec := &settingRecorder{}
	holds := func(string, string) CapSet { return oltp.CapSet("a") }
	c := &credRig{t: t, db: db, rec: rec}
	deps := Deps{Version: "v", SettingLog: rec, SecretBox: testBox(t), Acts: db, External: flakyExternal{db, c},
		Repos:    NewRepos(map[string]*Repo{"test": {Index: wikiIndex(t), Code: "code-test"}}),
		Accounts: adminAdapter{acctAdapter{db}}, SignOffs: flakySignOffs{testSignOffs{db}, c}, Prefs: db, CapsFor: holds}
	r := ops.NewRegistry()
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	// An agent holds no letter of its own here, so ci.token.new is
	// refused for it and can only be asked.
	agentHolds := func(who, repo string) CapSet {
		if who == "jva-bot" {
			return oltp.CapSet("")
		}
		return holds(who, repo)
	}
	h, err := RouterWithCaps(r, "v", agentHolds, nil)
	if err != nil {
		t.Fatal(err)
	}
	c.deps, c.r, c.h = deps, r, h
	return c
}

// ask files a request as agent over HTTP with Quarry-Ask and has its
// person approve it; the id comes back.
func (c *credRig) ask(agent, person, path, body string) string {
	c.t.Helper()
	ctx := ops.WithToken(ops.WithUser(context.Background(), agent), ops.TokenAuth{ID: "T",
		Caps: func(string) ops.Caps { return oltp.CapSet("") }})
	req := httptest.NewRequest("POST", path, strings.NewReader(body)).WithContext(ctx)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Quarry-Ask", "for a test")
	w := httptest.NewRecorder()
	c.h.ServeHTTP(w, req)
	var asked struct {
		SignOff SignOffRequest `json:"signoff"`
	}
	if err := json.Unmarshal(w.Body.Bytes(), &asked); err != nil || w.Code != 202 {
		c.t.Fatalf("FAIL: asking %s: %d %s", path, w.Code, w.Body)
	}
	c.approve(person, asked.SignOff.ID)
	return asked.SignOff.ID
}

func (c *credRig) approve(person, id string) {
	c.t.Helper()
	if _, err := c.r.Invoke(asUser(person), "signoff.decide", signOffDecideIn{ID: id, Approve: true}); err != nil {
		c.t.Fatalf("FAIL: approving %s: %v", id, err)
	}
}

// held reads the sealed secret without collecting it, as the forge
// itself could.
func (c *credRig) held(id string) string {
	c.t.Helper()
	row, _, err := c.db.SignOffByID(id)
	if err != nil || row.Secret == "" {
		c.t.Fatalf("FAIL: no held secret for %s: %v", id, err)
	}
	v, err := c.deps.openSignOffSecret(id, row.Secret)
	if err != nil {
		c.t.Fatal(err)
	}
	return v
}

func (c *credRig) value(id string) string {
	c.t.Helper()
	var v struct{ Value string }
	if err := json.Unmarshal([]byte(c.held(id)), &v); err != nil || v.Value == "" {
		c.t.Fatalf("FAIL: the held secret: %v", err)
	}
	return v.Value
}

func (c *credRig) sweep(after time.Duration) int {
	c.t.Helper()
	n, err := c.deps.LapseSignOffSecrets(time.Now().Add(after))
	if err != nil {
		c.t.Fatal(err)
	}
	return n
}

// lapseActs is the account-level receipts the sweep wrote for account.
func (c *credRig) lapseActs(account string) []oltp.ForgeAct {
	c.t.Helper()
	acts, err := c.db.AccountActs(account, 50)
	if err != nil {
		c.t.Fatal(err)
	}
	var out []oltp.ForgeAct
	for _, a := range acts {
		if strings.Contains(a.Actor, "lapsed uncollected") {
			out = append(out, a)
		}
	}
	return out
}

func (c *credRig) settingRows(field string) []logRow {
	var out []logRow
	for _, r := range c.rec.rows {
		if r.field == field && strings.Contains(r.now, "lapsed uncollected") {
			out = append(out, r)
		}
	}
	return out
}

const lateEnough = 25 * time.Hour

// An agent token an agent asked for and never collected is revoked
// when its secret lapses, once, with a receipt on the agent's account
// that names the request and the asker and not the value; a second
// sweep does nothing; one collected in time is never revoked.
func TestALapsedAgentTokenAskIsRevoked(t *testing.T) {
	c := newCredRig(t)
	body := `{"agent":"jva-bot","name":"s","scopes":"api"}`
	lost := c.ask("jva-bot", "jva", "/api/v1/agents/tokens", body)
	value := c.value(lost)
	if _, ok := c.db.ResolveToken("jva-bot", value, oltp.ScopeAPI); !ok {
		t.Fatal("FAIL: the minted token does not verify before the lapse")
	}
	kept := c.ask("jva-bot", "jva", "/api/v1/agents/tokens", `{"agent":"jva-bot","name":"kept","scopes":"api"}`)
	got, err := c.r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: kept})
	if err != nil || len(got.(signOffGetOut).Secret) == 0 {
		t.Fatalf("FAIL: collecting: %v", err)
	}
	var keptSecret struct{ Value string }
	if err := json.Unmarshal(got.(signOffGetOut).Secret, &keptSecret); err != nil {
		t.Fatal(err)
	}

	if n := c.sweep(0); n != 0 {
		t.Fatalf("FAIL: a fresh secret lapsed: %d", n)
	}
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: the sweep cleared %d, want 1", n)
	}
	if _, ok := c.db.ResolveToken("jva-bot", value, oltp.ScopeAPI); ok {
		t.Error("FAIL: the uncollected token still verifies")
	}
	if _, ok := c.db.ResolveToken("jva-bot", keptSecret.Value, oltp.ScopeAPI); !ok {
		t.Error("FAIL: a collected token was revoked")
	}
	acts := c.lapseActs("jva-bot")
	if len(acts) != 1 || !strings.Contains(acts[0].Actor, lost) || !strings.Contains(acts[0].Actor, "asked by jva-bot") {
		t.Fatalf("FAIL: receipts %+v", acts)
	}
	for _, a := range acts {
		if strings.Contains(a.Actor+a.Op, value) {
			t.Error("FAIL: a receipt holds the token value")
		}
	}
	if n := c.sweep(lateEnough + time.Hour); n != 0 || len(c.lapseActs("jva-bot")) != 1 {
		t.Errorf("FAIL: a second sweep did something: %d, %+v", n, c.lapseActs("jva-bot"))
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// An agent deleted before its token's lapse is not an error and
// leaves no receipt: there is nothing left to revoke.
func TestALapseForADeletedAgentIsQuiet(t *testing.T) {
	c := newCredRig(t)
	id := c.ask("jva-bot", "jva", "/api/v1/agents/tokens", `{"agent":"jva-bot","name":"s","scopes":"api"}`)
	due, err := c.db.DueSignOffLapses(time.Now().Add(lateEnough))
	if err != nil || len(due) != 1 || due[0].ID != id || !strings.Contains(due[0].Refs, lapseAgent) || !strings.Contains(due[0].Refs, "jva-bot") {
		t.Fatalf("FAIL: the references do not name an agent token: %+v %v", due, err)
	}
	if err := c.db.DeleteAccount("jva-bot"); err != nil {
		t.Fatal(err)
	}
	n, err := c.deps.LapseSignOffSecrets(time.Now().Add(lateEnough))
	if err != nil || n != 1 {
		t.Fatalf("FAIL: the sweep cleared %d for %s: %v", n, id, err)
	}
	if row, _, _ := c.db.SignOffByID(id); row.Secret != oltp.SignOffSecretLapsed {
		t.Errorf("FAIL: the row was not cleared: %q", row.Secret)
	}
	if got := c.lapseActs("jva-bot"); len(got) != 0 {
		t.Errorf("FAIL: receipts for a deleted agent: %+v", got)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A reissue's lapse revokes only the token it minted, and its receipt
// says the agent is left with none.
func TestALapsedReissueRevokesWhatItMinted(t *testing.T) {
	c := newCredRig(t)
	if _, _, err := c.db.MintToken("jva-bot", oltp.TokenSpec{Name: "old", Scopes: []string{oltp.ScopeAPI}}); err != nil {
		t.Fatal(err)
	}
	id := c.ask("jva-bot", "jva", "/api/v1/agents/tokens/reissue", `{"name":"jva-bot"}`)
	value := c.value(id)
	if _, ok := c.db.ResolveToken("jva-bot", value, oltp.ScopeAPI); !ok {
		t.Fatal("FAIL: the reissued token does not verify")
	}
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: swept %d", n)
	}
	if _, ok := c.db.ResolveToken("jva-bot", value, oltp.ScopeAPI); ok {
		t.Error("FAIL: the reissued token survived its lapse")
	}
	acts := c.lapseActs("jva-bot")
	if len(acts) != 1 || !strings.Contains(acts[0].Actor, "no live token") {
		t.Errorf("FAIL: receipts %+v", acts)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// An uncollected CI reporter token is revoked, logged in the
// repository's settings log; one collected in time stays.
func TestALapsedReporterTokenAskIsRevoked(t *testing.T) {
	c := newCredRig(t)
	lost := c.ask("jva-bot", "jva", "/api/v1/r/test/ci/tokens", `{"label":"ci"}`)
	var v struct{ Token string }
	if err := json.Unmarshal([]byte(c.held(lost)), &v); err != nil || v.Token == "" {
		t.Fatalf("FAIL: the held secret: %v", err)
	}
	kept := c.ask("jva-bot", "jva", "/api/v1/r/test/ci/tokens", `{"label":"kept"}`)
	got, err := c.r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: kept})
	if err != nil {
		t.Fatal(err)
	}
	var k struct{ Token string }
	if err := json.Unmarshal(got.(signOffGetOut).Secret, &k); err != nil {
		t.Fatal(err)
	}
	if !c.db.CheckReporterToken("test", v.Token) {
		t.Fatal("FAIL: the reporter token does not verify before the lapse")
	}
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: swept %d", n)
	}
	if c.db.CheckReporterToken("test", v.Token) {
		t.Error("FAIL: the uncollected reporter token still verifies")
	}
	if !c.db.CheckReporterToken("test", k.Token) {
		t.Error("FAIL: a collected reporter token was revoked")
	}
	rows := c.settingRows("reporter-token")
	if len(rows) != 1 || !strings.Contains(rows[0].now, lost) || strings.Contains(rows[0].was+rows[0].now, v.Token) {
		t.Errorf("FAIL: settings rows %+v", rows)
	}
	if n := c.sweep(lateEnough + time.Hour); n != 0 || len(c.settingRows("reporter-token")) != 1 {
		t.Errorf("FAIL: a second sweep did something: %d", n)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A plan that minted several credentials has every one revoked.
func TestALapsedPlanRevokesEveryCredentialItMinted(t *testing.T) {
	c := newCredRig(t)
	step := func(op, in string) planStepIn { return planStepIn{Op: op, Input: json.RawMessage(in)} }
	out, err := c.r.Invoke(asUser("fbot"), planOp, planIn{Title: "credentials", Steps: []planStepIn{
		step("agent.token.new", `{"agent":"fbot","name":"a","scopes":"api"}`),
		step("agent.token.new", `{"agent":"fbot","name":"b","scopes":"api"}`),
		step("ci.token.new", `{"repo":"test","label":"r"}`),
	}})
	if err != nil {
		t.Fatalf("FAIL: asking: %v", err)
	}
	id := out.(planOut).SignOff.ID
	c.approve("dmestas", id)
	var secrets map[string]json.RawMessage
	if err := json.Unmarshal([]byte(c.held(id)), &secrets); err != nil || len(secrets) != 3 {
		t.Fatalf("FAIL: the plan's secrets: %v %d", err, len(secrets))
	}
	var a, b struct{ Value string }
	var rep struct{ Token string }
	if json.Unmarshal(secrets["1"], &a) != nil || json.Unmarshal(secrets["2"], &b) != nil || json.Unmarshal(secrets["3"], &rep) != nil {
		t.Fatal("FAIL: reading the plan's secrets")
	}
	live := func() (bool, bool, bool) {
		_, x := c.db.ResolveToken("fbot", a.Value, oltp.ScopeAPI)
		_, y := c.db.ResolveToken("fbot", b.Value, oltp.ScopeAPI)
		return x, y, c.db.CheckReporterToken("test", rep.Token)
	}
	if x, y, z := live(); !x || !y || !z {
		t.Fatalf("FAIL: before the lapse: %v %v %v", x, y, z)
	}
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: swept %d", n)
	}
	if x, y, z := live(); x || y || z {
		t.Errorf("FAIL: after the lapse still live: %v %v %v", x, y, z)
	}
	if n := len(c.lapseActs("fbot")); n != 2 {
		t.Errorf("FAIL: %d account receipts, want 2", n)
	}
	if n := len(c.settingRows("reporter-token")); n != 1 {
		t.Errorf("FAIL: %d settings rows, want 1", n)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A revoke that fails leaves the secret and its reference in place and
// says so once; the next sweep finishes the job with one log row.
func TestAFailedRevokeIsRetriedByTheNextSweep(t *testing.T) {
	c := newCredRig(t)
	id := c.ask("jva-bot", "jva", "/api/v1/r/test/ci/tokens", `{"label":"ci"}`)
	var v struct{ Token string }
	if err := json.Unmarshal([]byte(c.held(id)), &v); err != nil || v.Token == "" {
		t.Fatalf("FAIL: the held secret: %v", err)
	}
	c.failRevoke = true
	n, err := c.deps.LapseSignOffSecrets(time.Now().Add(lateEnough))
	if err == nil || n != 0 {
		t.Fatalf("FAIL: a failed revoke answered %d, %v", n, err)
	}
	if strings.Contains(err.Error(), v.Token) {
		t.Error("FAIL: the failure names the token value")
	}
	// The row is lapsing: refused to a collector, references kept.
	got, gerr := c.r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: id})
	if gerr != nil || len(got.(signOffGetOut).Secret) != 0 || got.(signOffGetOut).Note == "" {
		t.Errorf("FAIL: a lapsing row was handed to its collector: %v %+v", gerr, got)
	}
	if due, _ := c.db.DueSignOffLapses(time.Now()); len(due) != 1 || due[0].Refs == "" {
		t.Errorf("FAIL: the reference is gone: %+v", due)
	}
	if !c.db.CheckReporterToken("test", v.Token) || len(c.settingRows("reporter-token")) != 0 {
		t.Error("FAIL: a failed revoke changed something")
	}
	c.failRevoke = false
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: the retry cleared %d", n)
	}
	if c.db.CheckReporterToken("test", v.Token) {
		t.Error("FAIL: the token survived the retry")
	}
	if n := len(c.settingRows("reporter-token")); n != 1 {
		t.Errorf("FAIL: %d log rows after the retry, want 1", n)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A clear that fails after a successful revoke is retried quietly: the
// token ends revoked, the row cleared, and exactly one log row.
func TestAFailedClearIsRetriedWithoutASecondLogRow(t *testing.T) {
	c := newCredRig(t)
	id := c.ask("jva-bot", "jva", "/api/v1/agents/tokens", `{"agent":"jva-bot","name":"s","scopes":"api"}`)
	value := c.value(id)
	c.failClear = true
	if n, err := c.deps.LapseSignOffSecrets(time.Now().Add(lateEnough)); err == nil || n != 0 {
		t.Fatalf("FAIL: a failed clear answered %d, %v", n, err)
	}
	if _, ok := c.db.ResolveToken("jva-bot", value, oltp.ScopeAPI); ok {
		t.Error("FAIL: the token was not revoked before the clear")
	}
	c.failClear = false
	if n := c.sweep(lateEnough); n != 1 {
		t.Fatalf("FAIL: the retry cleared %d", n)
	}
	row, _, _ := c.db.SignOffByID(id)
	if row.Secret != oltp.SignOffSecretLapsed {
		t.Errorf("FAIL: the secret column holds %q", row.Secret)
	}
	if n := len(c.lapseActs("jva-bot")); n != 1 {
		t.Errorf("FAIL: %d receipts, want 1", n)
	}
	if n := c.sweep(lateEnough + time.Hour); n != 0 {
		t.Errorf("FAIL: a further sweep cleared %d", n)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}