package api
import (
"context"
"encoding/json"
"net/http/httptest"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// RFC 0027 through the router: a forge agent refused agent.new asks its
// person instead; the person approves, the agent exists as theirs, and
// the agent reads what became of its request. Without Quarry-Ask, with
// a token limited to repositories, or for the person's own account, the
// refusal stands as it was.
func TestAnAgentAsksItsPersonForWhatItMayNotDo(t *testing.T) {
r, _, _, _, _ := signOffRig(t)
h, err := RouterWithCaps(r, "v", func(string, string) CapSet { return oltp.CapSet("") }, nil)
if err != nil {
t.Fatal(err)
}
agentCtx := func(limited bool) context.Context {
return ops.WithToken(ops.WithUser(context.Background(), "fbot"), ops.TokenAuth{ID: "T",
Caps: func(string) ops.Caps { return oltp.CapSet("") },
Limited: limited,
ForgeAgent: func() bool { return true }})
}
call := func(ctx context.Context, path, body, ask string) (int, string) {
req := httptest.NewRequest("POST", path, strings.NewReader(body)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
if ask != "" {
req.Header.Set("Quarry-Ask", ask)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code, rec.Body.String()
}
if c, _ := call(agentCtx(false), "/api/v1/agents", `{"name":"rajeev"}`, ""); c != 403 {
t.Fatalf("FAIL: without Quarry-Ask an agent's agent.new answered %d, not its refusal", c)
}
if c, _ := call(agentCtx(true), "/api/v1/agents", `{"name":"rajeev"}`, "a QA tester"); c != 403 {
t.Errorf("FAIL: a token limited to repositories asked for a forge-wide act: %d", c)
}
c, body := call(agentCtx(false), "/api/v1/agents", `{"name":"rajeev"}`, "a QA tester for dantest-sandbox")
if c != 202 {
t.Fatalf("FAIL: asking answered %d: %s", c, body)
}
var asked struct {
SignOff SignOffRequest `json:"signoff"`
}
if err := json.Unmarshal([]byte(body), &asked); err != nil || asked.SignOff.ID == "" ||
asked.SignOff.Owner != "dmestas" || asked.SignOff.State != "pending" || asked.SignOff.Reason == "" {
t.Fatalf("FAIL: the request: %s", body)
}
if !strings.Contains(asked.SignOff.Summary, "rajeev") || strings.Contains(asked.SignOff.Summary, "agent.new") {
t.Errorf("FAIL: the summary does not say what runs: %q", asked.SignOff.Summary)
}
// The person approves from their session; the call runs as them.
if _, err := r.Invoke(asUser("dmestas"), "signoff.decide", signOffDecideIn{ID: asked.SignOff.ID, Approve: true}); err != nil {
t.Fatalf("FAIL: approving: %v", err)
}
out, err := r.Invoke(asUser("fbot"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID})
if err != nil {
t.Fatal(err)
}
if g := out.(signOffGetOut); g.Request.State != "approved" || !strings.Contains(string(g.Result), `"rajeev"`) ||
!strings.Contains(string(g.Result), `"dmestas"`) {
t.Errorf("FAIL: the agent read state=%s result=%s", g.Request.State, g.Result)
}
// A token for the new agent: the value goes to the asking agent
// alone, once, and never into the answer its person reads.
c, body = call(agentCtx(false), "/api/v1/agents/tokens", `{"agent":"rajeev","name":"qa","scopes":"api,web,sync"}`, "rajeev's token")
if c != 202 {
t.Fatalf("FAIL: asking for a token answered %d: %s", c, body)
}
asked = struct {
SignOff SignOffRequest `json:"signoff"`
}{}
if err := json.Unmarshal([]byte(body), &asked); err != nil || !strings.Contains(asked.SignOff.Summary, "for rajeev") {
t.Fatalf("FAIL: the token request: %s", body)
}
if _, err := r.Invoke(asUser("dmestas"), "signoff.decide", signOffDecideIn{ID: asked.SignOff.ID, Approve: true}); err != nil {
t.Fatalf("FAIL: approving the token: %v", err)
}
seen, err := r.Invoke(asUser("dmestas"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID})
if err != nil {
t.Fatal(err)
}
if g := seen.(signOffGetOut); len(g.Secret) != 0 || strings.Contains(string(g.Result), `"value"`) {
t.Errorf("FAIL: the person read the token: result=%s secret=%s", g.Result, g.Secret)
}
first, _ := r.Invoke(asUser("fbot"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID})
var tok struct {
Value string `json:"value"`
}
if err := json.Unmarshal(first.(signOffGetOut).Secret, &tok); err != nil || len(tok.Value) < 20 {
t.Errorf("FAIL: the agent did not get its token: %s", first.(signOffGetOut).Secret)
}
if again, _ := r.Invoke(asUser("fbot"), "signoff.get", signOffGetIn{ID: asked.SignOff.ID}); len(again.(signOffGetOut).Secret) != 0 {
t.Error("FAIL: the token was readable twice")
}
// The person's own account is never askable.
if c, _ := call(agentCtx(false), "/api/v1/me/passwd", `{"password":"x"}`, "please"); c == 202 {
t.Error("FAIL: an agent asked for its person's password change")
}
}
// RFC 0027, Limits: a person can stop an agent asking; its ask is then
// refused with a sentence saying so. Only its person can flip it.
func TestAPersonCanStopAnAgentAsking(t *testing.T) {
r, _, _, _, _ := signOffRig(t)
h, err := RouterWithCaps(r, "v", func(string, string) CapSet { return oltp.CapSet("") }, nil)
if err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(asUser("mallory"), "agent.ask", agentAskIn{Agent: "fbot", On: false}); err == nil {
t.Fatal("FAIL: somebody else stopped fbot asking")
}
if _, err := r.Invoke(asUser("dmestas"), "agent.ask", agentAskIn{Agent: "fbot", On: false}); err != nil {
t.Fatal(err)
}
ctx := ops.WithToken(ops.WithUser(context.Background(), "fbot"), ops.TokenAuth{ID: "T",
Caps: func(string) ops.Caps { return oltp.CapSet("") }, ForgeAgent: func() bool { return true }})
req := httptest.NewRequest("POST", "/api/v1/agents", strings.NewReader(`{"name":"rajeev"}`)).WithContext(ctx)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Quarry-Ask", "a QA tester")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 403 {
t.Errorf("FAIL: an agent told not to ask was answered %d: %s", rec.Code, rec.Body.String())
}
out, err := r.Invoke(asUser("dmestas"), "agent.ask", agentAskIn{Agent: "fbot", On: true})
if err != nil || !out.(agentAskOut).Asks {
t.Fatalf("FAIL: turning it back on: %+v %v", out, err)
}
}