Skip to content
quarry Artifact [6ec4b63bc3]
Login

Artifact [6ec4b63bc3]

Artifact 6ec4b63bc39b1b39baadd8df1175ab916909cd885656221fe5e6522a367544ce:


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, "agent.new for rajeev") {
		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)
	}
}