package api
import (
"context"
"encoding/base64"
"encoding/json"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/cisecret"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
func testBox(t *testing.T) cisecret.Box {
t.Helper()
box, err := cisecret.New(base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef")))
if err != nil {
t.Fatal(err)
}
return box
}
// An approved request's secret is sealed at rest, handed to the asking
// agent once, and, uncollected, cleared after its limit with the token
// it held revoked and the lapse logged; a second sweep does nothing.
func TestASignOffSecretIsSealedCollectedOnceAndLapses(t *testing.T) {
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{"jva"} {
if err := db.AddAccount(who, "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}
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)
}
ctx := ops.WithToken(ops.WithUser(context.Background(), "jva-bot"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
ask := func(label string) string {
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")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
if err := json.Unmarshal(w.Body.Bytes(), &asked); err != nil || w.Code != 202 {
t.Fatalf("FAIL: asking: %d %s", w.Code, w.Body)
}
if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: asked.SignOff.ID, Approve: true}); err != nil {
t.Fatal(err)
}
return asked.SignOff.ID
}
// Collected: sealed at rest, the value once.
first := ask("one")
row, _, _ := db.SignOffByID(first)
if !strings.HasPrefix(row.Secret, sealedPrefix) || strings.Contains(row.Secret, "s3cret-bearer-value") {
t.Fatalf("FAIL: the stored column is not sealed: %.60s", row.Secret)
}
got, err := r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: first})
if err != nil || !strings.Contains(string(got.(signOffGetOut).Secret), "s3cret-bearer-value") {
t.Fatalf("FAIL: the agent did not get the value: %v %s", err, got.(signOffGetOut).Secret)
}
again, _ := r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: first})
if len(again.(signOffGetOut).Secret) != 0 {
t.Error("FAIL: the value was readable twice")
}
// Uncollected: cleared after the limit, the token revoked, logged once.
second := ask("two")
before := len(eng.revoked)
if n, err := deps.LapseSignOffSecrets(time.Now()); err != nil || n != 0 {
t.Fatalf("FAIL: a fresh secret lapsed: %d %v", n, err)
}
if n, err := deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 {
t.Fatalf("FAIL: the sweep cleared %d (%v), want 1", n, err)
}
if len(eng.revoked) != before+1 || eng.revoked[len(eng.revoked)-1] != "new-2" {
t.Errorf("FAIL: revoked %v, want new-2 added", eng.revoked)
}
var lapsedRows int
for _, row := range rec.rows {
if strings.Contains(row.now, "lapsed uncollected") && strings.Contains(row.now, second) && strings.Contains(row.now, "jva-bot") {
lapsedRows++
}
}
if lapsedRows != 1 {
t.Errorf("FAIL: %d lapse rows in %+v", lapsedRows, rec.rows)
}
row, _, _ = db.SignOffByID(second)
if row.Secret != oltp.SignOffSecretLapsed {
t.Errorf("FAIL: the secret column holds %q after the sweep", row.Secret)
}
if n, _ := deps.LapseSignOffSecrets(time.Now().Add(26 * time.Hour)); n != 0 || len(eng.revoked) != before+1 {
t.Errorf("FAIL: a second sweep did something: %d, revoked %v", n, eng.revoked)
}
late, err := r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: second})
if err != nil || late.(signOffGetOut).Note == "" || len(late.(signOffGetOut).Secret) != 0 {
t.Errorf("FAIL: the agent reading after the limit: %v %+v", err, late)
}
// A row written before secrets were sealed is still handed over once.
third := ask("three")
if err := db.FinishSignOff(third, `{}`, `{"value":"legacy-plain"}`); err != nil {
t.Fatal(err)
}
old, _ := r.Invoke(asUser("jva-bot"), "signoff.get", signOffGetIn{ID: third})
if !strings.Contains(string(old.(signOffGetOut).Secret), "legacy-plain") {
t.Errorf("FAIL: an unsealed row was not readable: %s", old.(signOffGetOut).Secret)
}
t.Log("ok")
}