package api
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"log"
"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"
)
// missLimiter is the sign-in limiter's shape for a test: five free
// misses per name and per address, then held until release.
type missLimiter struct {
mu sync.Mutex
miss map[string]int
held bool
}
func (l *missLimiter) Allowed(user, ip string) (bool, time.Duration) {
l.mu.Lock()
defer l.mu.Unlock()
if l.miss["a:"+ip] > 5 {
return false, 30 * time.Second
}
return true, 0
}
func (l *missLimiter) Failed(user, ip string) {
l.mu.Lock()
defer l.mu.Unlock()
l.miss["u:"+user]++
l.miss["a:"+ip]++
}
func (l *missLimiter) Succeeded(user, ip string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.miss, "u:"+user)
delete(l.miss, "a:"+ip)
}
func (l *missLimiter) release() {
l.mu.Lock()
defer l.mu.Unlock()
l.miss = map[string]int{}
}
type pickupRig struct {
db *oltp.DB
deps Deps
r *ops.Registry
h http.Handler
coll *PickupCollector
lim *missLimiter
eng *workerEngine
rec *settingRecorder
ctx context.Context
}
func newPickupRig(t *testing.T) pickupRig { return newPickupRigWith(t, nil) }
func newPickupRigWith(t *testing.T, tweak func(*Deps)) pickupRig {
t.Helper()
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
if err := db.AddAccount("jva", "a-long-enough-password", false); 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)
}
eng := &workerEngine{}
rec := &settingRecorder{}
holds := func(string, string) CapSet { return oltp.CapSet("a") }
deps := Deps{Version: "v", WorkerTokens: eng, SettingLog: rec, SecretBox: testBox(t),
Repos: NewRepos(map[string]*Repo{"test": {Index: wikiIndex(t), Code: "code-test"}}),
Accounts: adminAdapter{acctAdapter{db}}, SignOffs: testSignOffs{db}, CapsFor: holds}
if tweak != nil {
tweak(&deps)
}
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{}}
ctx := ops.WithToken(ops.WithUser(context.Background(), "jva-bot"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
return pickupRig{db: db, deps: deps, r: r, h: h, coll: NewPickupCollector(deps, lim, nil), lim: lim, eng: eng, rec: rec, ctx: ctx}
}
func newPickupKey(t *testing.T) (key, code string) {
t.Helper()
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
t.Fatal(err)
}
return base64.RawURLEncoding.EncodeToString(raw), PickupCodeOf(raw)
}
// ask files a worker-token request as the agent, bound to code when it
// names one, and returns its id.
func (g pickupRig) ask(t *testing.T, label, code string) (int, string) {
t.Helper()
req := httptest.NewRequest("POST", "/api/v1/r/test/ci/worker-tokens", strings.NewReader(`{"label":"`+label+`"}`)).WithContext(g.ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Ask", "a runner")
if code != "" {
req.Header.Set("Quarry-Pickup", code)
}
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
_ = json.Unmarshal(w.Body.Bytes(), &asked)
return w.Code, asked.SignOff.ID
}
func (g pickupRig) decide(t *testing.T, id string, approve bool) {
t.Helper()
if _, err := g.r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: id, Approve: approve}); err != nil {
t.Fatal(err)
}
}
// collect sends the collect call with an Authorization header, and
// returns the status and the body.
func (g pickupRig) collect(auth, path, query string) (int, string) {
req := httptest.NewRequest("POST", path+query, nil)
if auth != "" {
req.Header.Set("Authorization", auth)
}
req.RemoteAddr = "192.0.2.7:4000"
if i := strings.Index(path, "/r/"); i >= 0 {
parts := strings.Split(strings.Trim(path[i:], "/"), "/")
req.SetPathValue("repo", parts[1])
}
w := httptest.NewRecorder()
g.coll.ServeHTTP(w, req)
return w.Code, w.Body.String()
}
const collectPath = "/api/v1/pickups/collect"
// A request asked with a pickup code is approved and collected with the
// key exactly once; the asker's own signoff.get never has the secret,
// before or after; every other answer is the RFC's.
func TestAPickupBoundRequestIsCollectedOnceAndNeverThroughSignoffGet(t *testing.T) {
var logs bytes.Buffer
log.SetOutput(&logs)
defer log.SetOutput(log.Writer())
g := newPickupRig(t)
key, code := newPickupKey(t)
auth := "Pickup " + key
status, id := g.ask(t, "one", code)
if status != 202 || id == "" {
t.Fatalf("FAIL: asking with a pickup code: %d", status)
}
row, _, _ := g.db.SignOffByID(id)
if row.Pickup != code {
t.Fatalf("FAIL: the request does not hold the code")
}
if got, err := g.r.Invoke(asUser("jva"), "signoff.get", signOffGetIn{ID: id}); err != nil || got.(signOffGetOut).Request.Pickup != code {
t.Errorf("FAIL: the owner's view of the request lacks the bound code: %v", err)
}
// Not yet decided: the key holder is told it waits; a wrong key,
// an unknown code and a malformed header are one answer.
if st, body := g.collect(auth, collectPath, ""); st != 202 || strings.Contains(body, key) {
t.Errorf("FAIL: undecided: %d", st)
}
wrong, _ := newPickupKey(t)
wrongSt, wrongBody := g.collect("Pickup "+wrong, collectPath, "")
for name, a := range map[string]string{"short": "Pickup abc", "scheme": "Bearer " + key, "none": "", "junk": "Pickup !!!!"} {
if st, body := g.collect(a, collectPath, ""); st != wrongSt || body != wrongBody {
t.Errorf("FAIL: %s answered %d %s, a wrong key %d %s", name, st, body, wrongSt, wrongBody)
}
}
if wrongSt != 404 {
t.Errorf("FAIL: a wrong key answered %d", wrongSt)
}
// A key in a query string is refused before it is read.
if st, _ := g.collect("", collectPath, "?key="+key); st != 400 {
t.Errorf("FAIL: a query string answered %d", st)
}
g.decide(t, id, true)
// signoff.get by the asker never has the secret, and leaves it.
for i := 0; i < 2; i++ {
got, err := g.r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: id})
if err != nil || len(got.(signOffGetOut).Secret) != 0 || strings.Contains(string(got.(signOffGetOut).Result), "s3cret-bearer-value") {
t.Fatalf("FAIL: signoff.get gave the asker the secret: %v %+v", err, got)
}
}
g.lim.release()
// The wrong key still gets the generic answer and takes nothing.
if st, body := g.collect("Pickup "+wrong, collectPath, ""); st != wrongSt || body != wrongBody {
t.Errorf("FAIL: a wrong key after approval: %d %s", st, body)
}
st, body := g.collect(auth, collectPath, "")
var out struct {
Request string `json:"request"`
Op string `json:"op"`
Agent string `json:"agent"`
Owner string `json:"owner"`
Repo string `json:"repo"`
Secret json.RawMessage `json:"secret"`
}
if err := json.Unmarshal([]byte(body), &out); err != nil || st != 200 || out.Request != id || out.Agent != "jva-bot" || out.Owner != "jva" || out.Op != "ci.worker.token.new" || out.Repo != "test" ||
!strings.Contains(string(out.Secret), "s3cret-bearer-value") {
t.Fatalf("FAIL: collecting: %d %s", st, body)
}
st, body = g.collect(auth, collectPath, "")
if st != 410 || strings.Contains(body, "s3cret-bearer-value") || !strings.Contains(body, `"collected"`) {
t.Errorf("FAIL: collecting twice: %d %s", st, body)
}
after, _ := g.r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: id})
if len(after.(signOffGetOut).Secret) != 0 {
t.Errorf("FAIL: signoff.get gave the secret after collection")
}
// Nothing for a lapse to revoke once collected.
if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(48 * time.Hour)); err != nil || n != 0 || len(g.eng.revoked) != 0 {
t.Errorf("FAIL: a sweep after collection: %d %v revoked %v", n, err, g.eng.revoked)
}
for _, secret := range []string{key, strings.TrimRight(key, "=")} {
if strings.Contains(logs.String(), secret) {
t.Errorf("FAIL: the key is in the log")
}
}
if strings.Contains(logs.String(), "s3cret-bearer-value") {
t.Errorf("FAIL: the value is in the log")
}
if !t.Failed() {
t.Log("ok")
}
}
// Denied, an uncollected lapse, the sync URL's route and an unfinished
// ask, each as the key holder is told.
func TestAPickupAnswersForADeniedALapsedAndAMisdirectedRequest(t *testing.T) {
g := newPickupRig(t)
denyKey, denyCode := newPickupKey(t)
_, denied := g.ask(t, "deny", denyCode)
g.decide(t, denied, false)
if st, body := g.collect("Pickup "+denyKey, collectPath, ""); st != 410 || !strings.Contains(body, `"denied"`) {
t.Errorf("FAIL: a denied request: %d %s", st, body)
}
if st, body := g.collect("Pickup "+denyKey, collectPath, ""); st != 410 {
t.Errorf("FAIL: a denied request again: %d %s", st, body)
}
key, code := newPickupKey(t)
_, id := g.ask(t, "lapse", code)
g.decide(t, id, true)
// The sync URL's route: a repository the credential does not reach
// consumes nothing; the one it was minted for collects.
if st, _ := g.collect("Pickup "+key, "/r/elsewhere/pickup", ""); st != 409 {
t.Errorf("FAIL: a repository the credential does not reach: %d", st)
}
if row, _, _ := g.db.SignOffByID(id); row.Secret == "" || row.Collected != "" {
t.Fatalf("FAIL: a refused route consumed the secret")
}
// Uncollected past its limit: revoked exactly as an unbound one is,
// and the key holder is told it lapsed.
if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 {
t.Fatalf("FAIL: the sweep cleared %d (%v)", n, err)
}
if len(g.eng.revoked) != 1 {
t.Errorf("FAIL: revoked %v", g.eng.revoked)
}
lapses := 0
for _, row := range g.rec.rows {
if strings.Contains(row.now, "lapsed uncollected") && strings.Contains(row.now, id) {
lapses++
}
}
if lapses != 1 {
t.Errorf("FAIL: %d lapse rows", lapses)
}
if st, body := g.collect("Pickup "+key, collectPath, ""); st != 410 || !strings.Contains(body, `"lapsed"`) || strings.Contains(body, "s3cret-bearer-value") {
t.Errorf("FAIL: a lapsed request: %d %s", st, body)
}
if st, _ := g.collect("Pickup "+key, "/r/elsewhere/pickup", ""); st != 410 {
t.Errorf("FAIL: a lapsed request on a repository it does not reach: %d, want 410", st)
}
if !t.Failed() {
t.Log("ok")
}
}
// The sync URL's route collects for a repository the credential reaches.
func TestAPickupCollectsOnASyncURLForTheRepositoryItReaches(t *testing.T) {
g := newPickupRig(t)
key, code := newPickupKey(t)
_, id := g.ask(t, "sync", code)
g.decide(t, id, true)
if st, body := g.collect("Pickup "+key, "/r/test/pickup", ""); st != 200 || !strings.Contains(body, "s3cret-bearer-value") {
t.Errorf("FAIL: collecting on the repository's own URL: %d %s", st, body)
}
if st, _ := g.collect("Pickup "+key, "/r/test/pickup", ""); st != 410 {
t.Errorf("FAIL: collecting twice: %d", st)
}
if !t.Failed() {
t.Log("ok")
}
}
// Holds fall on wrong keys alone: past the free misses an address's
// wrong keys are held (429), and the right key is still served, since a
// held address is shared and a key cannot be guessed.
func TestAPickupHoldsFallOnMissesNotOnTheRightKey(t *testing.T) {
g := newPickupRig(t)
key, code := newPickupKey(t)
_, id := g.ask(t, "limit", code)
g.decide(t, id, true)
for i := 0; i < 6; i++ {
bad, _ := newPickupKey(t)
if st, _ := g.collect("Pickup "+bad, collectPath, ""); st != 404 {
t.Fatalf("FAIL: wrong key %d answered %d", i, st)
}
}
bad, _ := newPickupKey(t)
if st, _ := g.collect("Pickup "+bad, collectPath, ""); st != 429 {
t.Errorf("FAIL: a wrong key from a held address: %d", st)
}
if st, body := g.collect("Pickup "+key, collectPath, ""); st != 200 || !strings.Contains(body, "s3cret-bearer-value") {
t.Errorf("FAIL: the right key from a held address: %d %s", st, body)
}
if !t.Failed() {
t.Log("ok")
}
}
// The plain rates bind every call, a right key too: ten a minute from
// one address and six a minute for one key, each clearing with the
// window.
func TestAPickupCallsAreRateLimited(t *testing.T) {
g := newPickupRig(t)
at := time.Now()
g.coll.now = func() time.Time { return at }
key, code := newPickupKey(t)
_, id := g.ask(t, "rate", code)
for i := 0; i < pickupPerCode; i++ {
if st, _ := g.collect("Pickup "+key, collectPath, ""); st != 202 {
t.Fatalf("FAIL: poll %d answered %d", i, st)
}
}
if st, _ := g.collect("Pickup "+key, collectPath, ""); st != 429 {
t.Errorf("FAIL: a poll past the per-key rate: %d", st)
}
// Misses spend the address's ten; a right key does not.
g.lim.release()
for i := 0; i < pickupPerAddress; i++ {
other, _ := newPickupKey(t)
g.lim.release()
if st, _ := g.collect("Pickup "+other, collectPath, ""); st != 404 {
t.Fatalf("FAIL: miss %d: %d", i, st)
}
}
other, _ := newPickupKey(t)
if st, _ := g.collect("Pickup "+other, collectPath, ""); st != 429 {
t.Errorf("FAIL: a miss past the per-address rate: %d", st)
}
otherKey, otherCode := newPickupKey(t)
_, oid := g.ask(t, "second", otherCode)
g.decide(t, oid, true)
if st, _ := g.collect("Pickup "+otherKey, collectPath, ""); st != 200 {
t.Errorf("FAIL: a right key from an address whose misses are spent: %d", st)
}
g.coll.now = func() time.Time { return at.Add(2 * time.Minute) }
g.decide(t, id, true)
if st, _ := g.collect("Pickup "+key, collectPath, ""); st != 200 {
t.Errorf("FAIL: after the window: %d", st)
}
if !t.Failed() {
t.Log("ok")
}
}
// countingSignOffs counts the takes, the only write a collect makes.
type countingSignOffs struct {
testSignOffs
takes *int
}
func (c countingSignOffs) TakePickupSecret(code string, now time.Time) (PickupTake, error) {
*c.takes++
return c.testSignOffs.TakePickupSecret(code, now)
}
// Pending and every closed state are answered from a read: the take,
// which opens a write transaction, is made only for an approved request
// that holds a secret.
func TestAPickupOnlyTakesWhenThereIsASecretToTake(t *testing.T) {
g := newPickupRig(t)
takes := 0
d := g.deps
d.SignOffs = countingSignOffs{testSignOffs{g.db}, &takes}
g.coll = NewPickupCollector(d, g.lim, nil)
pendKey, pendCode := newPickupKey(t)
g.ask(t, "pending", pendCode)
denyKey, denyCode := newPickupKey(t)
_, denied := g.ask(t, "denied", denyCode)
g.decide(t, denied, false)
if st, _ := g.collect("Pickup "+pendKey, collectPath, ""); st != 202 {
t.Errorf("FAIL: pending: %d", st)
}
if st, _ := g.collect("Pickup "+denyKey, collectPath, ""); st != 410 {
t.Errorf("FAIL: denied: %d", st)
}
if takes != 0 {
t.Errorf("FAIL: %d takes for requests with nothing to take", takes)
}
key, code := newPickupKey(t)
_, id := g.ask(t, "ready", code)
g.decide(t, id, true)
g.collect("Pickup "+key, collectPath, "")
g.collect("Pickup "+key, collectPath, "")
if takes != 1 {
t.Errorf("FAIL: %d takes for one secret collected twice", takes)
}
if !t.Failed() {
t.Log("ok")
}
}
// A sealed value the forge cannot open is not consumed: the collector
// is told of a fault, the row keeps its secret and its references, and
// the lapse revokes it.
func TestAPickupDoesNotTakeWhatItCannotUnseal(t *testing.T) {
g := newPickupRig(t)
key, code := newPickupKey(t)
_, id := g.ask(t, "broken", code)
g.decide(t, id, true)
if err := g.db.FinishSignOff(id, `{}`, "sealed:AAAA"); err != nil {
t.Fatal(err)
}
if st, _ := g.collect("Pickup "+key, collectPath, ""); st != 500 {
t.Errorf("FAIL: an unopenable value answered %d", st)
}
row, _, _ := g.db.SignOffByID(id)
if row.Secret == "" || row.Collected != "" {
t.Errorf("FAIL: the value was consumed")
}
if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 || len(g.eng.revoked) != 1 {
t.Errorf("FAIL: the lapse did not revoke it: %d %v %v", n, err, g.eng.revoked)
}
if !t.Failed() {
t.Log("ok")
}
}
// The code is checked where it enters and fixed at the first ask: the
// same code again is the same request; another code, or one where there
// was none, or none where there was one, is refused and the request
// stands as it was; a code in use is refused in the one sentence.
func TestAPickupCodeIsCheckedAndFixedAtTheAsk(t *testing.T) {
g := newPickupRig(t)
if st, _ := g.ask(t, "bad", "not-a-code"); st != 400 {
t.Errorf("FAIL: a malformed code was admitted: %d", st)
}
_, code := newPickupKey(t)
_, other := newPickupKey(t)
st, id := g.ask(t, "same", code)
if st != 202 {
t.Fatalf("FAIL: %d", st)
}
if st, again := g.ask(t, "same", code); st != 202 || again != id {
t.Errorf("FAIL: asking again with the same code: %d %s", st, again)
}
if st, _ := g.ask(t, "same", other); st != 400 {
t.Errorf("FAIL: asking again with another code: %d", st)
}
if st, _ := g.ask(t, "same", ""); st != 400 {
t.Errorf("FAIL: asking again with no code where one is bound: %d", st)
}
if row, _, _ := g.db.SignOffByID(id); row.Pickup != code {
t.Errorf("FAIL: the code changed")
}
// Unbound, then asked again with a code: refused, and the secret
// stays the asker's own.
if st, plain := g.ask(t, "plain", ""); st != 202 || plain == "" {
t.Fatalf("FAIL: %d", st)
}
if st, _ := g.ask(t, "plain", other); st != 400 {
t.Errorf("FAIL: an unbound request re-asked with a code: %d", st)
}
if st, _ := g.ask(t, "second", code); st != 400 {
t.Errorf("FAIL: one code bound to two requests: %d", st)
}
// The plan op takes it as a field.
if _, err := g.r.Invoke(g.ctx, planOp, planIn{Title: "t", Pickup: "short", Steps: []planStepIn{{Op: "ci.worker.token.new", Input: json.RawMessage(`{"repo":"test","label":"p"}`)}}}); err == nil {
t.Errorf("FAIL: a plan with a malformed code was admitted")
}
if !t.Failed() {
t.Log("ok")
}
}
// A pickup code without the ask header is refused before the call runs.
func TestAPickupCodeWithoutAskingIsRefusedBeforeTheCallRuns(t *testing.T) {
g := newPickupRig(t)
_, code := newPickupKey(t)
req := httptest.NewRequest("POST", "/api/v1/r/test/ci/worker-tokens", strings.NewReader(`{"label":"x"}`)).WithContext(g.ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Pickup", code)
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
if w.Code != 400 || g.eng.mints != 0 {
t.Errorf("FAIL: %d, %d tokens minted", w.Code, g.eng.mints)
}
if !t.Failed() {
t.Log("ok")
}
}
// runAs sends a worker-token call as account who with the ask and pickup
// headers, and returns the recorder.
func (g pickupRig) runAs(who, label, code string) *httptest.ResponseRecorder {
ctx := ops.WithToken(ops.WithUser(context.Background(), who), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
req := httptest.NewRequest("POST", "/api/v1/r/test/ci/worker-tokens", strings.NewReader(`{"label":"`+label+`"}`)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Ask", "a runner")
req.Header.Set("Quarry-Pickup", code)
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
return w
}
// A call its caller may make outright, sent with a pickup code, parks
// its credential: 202 and no secret anywhere in the answer, the key
// collects the value once, signoff.get never has it, and uncollected it
// lapses and the credential is revoked.
func TestAPickupParksTheSecretOfACallThatRunsOutright(t *testing.T) {
var logs bytes.Buffer
log.SetOutput(&logs)
defer log.SetOutput(log.Writer())
g := newPickupRig(t)
key, code := newPickupKey(t)
w := g.runAs("jva", "parked", code)
if w.Code != 202 || g.eng.mints != 1 || w.Header().Get("Quarry-Pickup") != "parked" || strings.Contains(w.Body.String(), "s3cret-bearer-value") {
t.Fatalf("FAIL: %d, %d minted, header %q, body %s", w.Code, g.eng.mints, w.Header().Get("Quarry-Pickup"), w.Body)
}
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
if err := json.Unmarshal(w.Body.Bytes(), &asked); err != nil || asked.SignOff.ID == "" || asked.SignOff.State != oltp.SignOffRan || asked.SignOff.Pickup != code {
t.Fatalf("FAIL: the parked request: %v %+v", err, asked.SignOff)
}
row, _, _ := g.db.SignOffByID(asked.SignOff.ID)
if !strings.HasPrefix(row.Secret, sealedPrefix) || strings.Contains(row.Secret, "s3cret-bearer-value") {
t.Errorf("FAIL: the parked secret is not sealed")
}
gotGet, err := g.r.Invoke(asUser("jva"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID})
if err != nil || len(gotGet.(signOffGetOut).Secret) != 0 {
t.Errorf("FAIL: signoff.get gave the parked secret: %v", err)
}
st, body := g.collect("Pickup "+key, collectPath, "")
var got200 map[string]any
_ = json.Unmarshal([]byte(body), &got200)
if st != 200 || !strings.Contains(body, "s3cret-bearer-value") || got200["parked"] != true || got200["answerable"] != "jva" || got200["owner"] != nil {
t.Errorf("FAIL: collecting the parked value: %d %s", st, body)
}
if list, err := g.r.Invoke(asUser("jva"), "signoff.list", signOffListIn{}); err != nil {
t.Error(err)
} else {
waiting, ran := 0, 0
for _, q := range list.(signOffListOut).Requests {
if q.State == "pending" {
waiting++
}
if q.State == oltp.SignOffRan {
ran++
}
}
if waiting != 0 || ran != 1 {
t.Errorf("FAIL: the owner's list shows %d waiting and %d ran", waiting, ran)
}
}
if st, _ := g.collect("Pickup "+key, collectPath, ""); st != 410 {
t.Errorf("FAIL: collecting twice: %d", st)
}
// Uncollected: lapses and is revoked.
_, code2 := newPickupKey(t)
if w := g.runAs("jva", "lapsing", code2); w.Code != 202 {
t.Fatalf("FAIL: %d %s", w.Code, w.Body)
}
if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 || len(g.eng.revoked) != 1 || g.eng.revoked[0] != "new-2" {
t.Errorf("FAIL: the lapse: %d %v revoked %v", n, err, g.eng.revoked)
}
if strings.Contains(logs.String(), "s3cret-bearer-value") {
t.Errorf("FAIL: the value is in the log")
}
if !t.Failed() {
t.Log("ok")
}
}
// A code another agent's request holds is refused before the call runs;
// the caller's own pending ask with it is answered as that ask, never
// run; and a call that runs but cannot be parked (here, no secret key to
// seal with) has its credential revoked at once, the caller told it was
// undone, without the secret.
func TestAPickupParkingFailureRevokesAndARefusedCodeNeverRuns(t *testing.T) {
g := newPickupRig(t)
_, taken := newPickupKey(t)
if st, _ := g.ask(t, "holder", taken); st != 202 {
t.Fatalf("FAIL: %d", st)
}
if w := g.runAs("jva", "refused", taken); w.Code != 400 || g.eng.mints != 0 {
t.Errorf("FAIL: a held code: %d, %d minted before it was refused", w.Code, g.eng.mints)
}
_, mine := newPickupKey(t)
if _, err := g.db.RequestSignOff(oltp.SignOff{Agent: "jva", Owner: "jva", Op: "x", Input: "{}", Summary: "x", Pickup: mine}); err != nil {
t.Fatal(err)
}
if w := g.runAs("jva", "own", mine); w.Code != 400 || g.eng.mints != 0 {
t.Errorf("FAIL: the caller's own pending code: %d, %d minted", w.Code, g.eng.mints)
}
// No secret key: sealing fails after the mint.
b := newPickupRigWith(t, func(d *Deps) { d.SecretBox = cisecret.Box{} })
_, code := newPickupKey(t)
w := b.runAs("jva", "undone", code)
if w.Code != 503 || b.eng.mints != 1 || len(b.eng.revoked) != 1 || strings.Contains(w.Body.String(), "s3cret-bearer-value") || !strings.Contains(w.Body.String(), "revoked") {
t.Errorf("FAIL: parking failure: %d, %d minted, revoked %v, body %s", w.Code, b.eng.mints, b.eng.revoked, w.Body)
}
if !t.Failed() {
t.Log("ok")
}
}
// Two callers presenting one free code at once: exactly one parks, and
// every credential minted for the other is revoked or never minted, so
// none is left live that nobody may collect.
func TestAPickupTwoCallersOneCodeLeaveExactlyOneLive(t *testing.T) {
g := newPickupRig(t)
_, code := newPickupKey(t)
const callers = 8
codes := make([]int, callers)
var wg sync.WaitGroup
for i := 0; i < callers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
codes[i] = g.runAs("jva", "c"+string(rune('a'+i)), code).Code
}()
}
wg.Wait()
parked := 0
for _, c := range codes {
if c == 202 {
parked++
} else if c != 400 {
t.Errorf("FAIL: a caller was answered %d", c)
}
}
g.eng.mu.Lock()
live := g.eng.mints - len(g.eng.revoked)
g.eng.mu.Unlock()
if parked != 1 || live != 1 {
t.Errorf("FAIL: %d parked, %d credentials live", parked, live)
}
if !t.Failed() {
t.Log("ok")
}
}
// Only the ops whose credential a lapse can revoke are parkable, and
// that is read from the one table, not kept by hand: of the seven ops
// that answer a once-shown secret, the four lapseOps names are parkable
// and the rest are refused.
func TestAPickupParkableOpsAreTheOnesALapseCanRevoke(t *testing.T) {
g := newPickupRig(t)
var once, parkableOps []string
for _, d := range g.r.All() {
if d.HasOnceSecret() {
once = append(once, d.Name)
if pickupAllowedFor(d) == nil {
parkableOps = append(parkableOps, d.Name)
}
}
}
if len(once) != 7 {
t.Errorf("FAIL: %d ops answer a once-shown secret: %v", len(once), once)
}
if len(parkableOps) != len(lapseOps) {
t.Errorf("FAIL: parkable %v, lapse table has %d", parkableOps, len(lapseOps))
}
for _, n := range parkableOps {
if lapseOps[n] == nil {
t.Errorf("FAIL: %s is parkable but nothing can revoke it", n)
}
}
for _, n := range []string{"me.sync.token", "account.invite"} {
if d, ok := g.r.Lookup(n); !ok || pickupAllowedFor(d) == nil {
t.Errorf("FAIL: %s is not refused", n)
}
}
if !t.Failed() {
t.Log("ok")
}
}
// A flood of random keys from one address leaves the limiter's memory
// bounded and charges nothing to a key it did not find; a right key from
// another address is served.
func TestAPickupAFloodOfKeysLeavesBoundedState(t *testing.T) {
g := newPickupRig(t)
key, code := newPickupKey(t)
_, id := g.ask(t, "flood", code)
g.decide(t, id, true)
for i := 0; i < 20000; i++ {
bad, _ := newPickupKey(t)
g.collect("Pickup "+bad, collectPath, "")
}
if n := g.coll.rates.size(); n > 2 {
t.Errorf("FAIL: %d rate entries after a flood of keys from one address, want the address's alone", n)
}
// The map is bounded even for many addresses.
at := time.Now()
for i := 0; i < 20000; i++ {
g.coll.rates.allow("a:"+newPickupAddr(i), pickupPerAddress, at)
}
if n := g.coll.rates.size(); n > pickupRatesCap {
t.Errorf("FAIL: %d rate entries, cap %d", n, pickupRatesCap)
}
req := httptest.NewRequest("POST", collectPath, nil)
req.Header.Set("Authorization", "Pickup "+key)
req.RemoteAddr = "198.51.100.9:1"
w := httptest.NewRecorder()
g.coll.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("FAIL: a right key from another address: %d", w.Code)
}
if !t.Failed() {
t.Log("ok")
}
}
func newPickupAddr(i int) string {
return "10.0." + string(rune('a'+i%26)) + "." + string(rune('a'+(i/26)%26)) + string(rune('a'+(i/676)%26))
}
// A pickup code on a call whose answer holds no credential is refused
// before the call runs.
func TestAPickupCodeOnACallWithNothingToCollectIsRefusedBeforeItRuns(t *testing.T) {
g := newPickupRig(t)
_, code := newPickupKey(t)
ctx := ops.WithToken(ops.WithUser(context.Background(), "jva"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
req := httptest.NewRequest("POST", "/api/v1/agents", strings.NewReader(`{"name":"zz-bot"}`)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Ask", "x")
req.Header.Set("Quarry-Pickup", code)
w := httptest.NewRecorder()
g.h.ServeHTTP(w, req)
if w.Code != 400 {
t.Errorf("FAIL: %d %s", w.Code, w.Body)
}
if _, ok := g.deps.accountNamed("zz-bot"); ok {
t.Errorf("FAIL: the call ran")
}
if !t.Failed() {
t.Log("ok")
}
}