Skip to content
quarry Artifact [b2b14700a2]
Login

Artifact [b2b14700a2]

Artifact b2b14700a2de9c651466bb2004640118c8dae59c9ede497e25d53228a2c0f451:


package api

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

// A gated op must say what its letters are ABOUT.
//
// Four incidents this week share one shape: a gate resolved its subject
// to "" and asked anyway. The caps lookup answered confidently about the
// fleet, the caller got a 403 naming a capability and a grantor, and it
// read exactly like a policy decision — b683c54b sat open two days on
// that disguise. The subject is the thing nobody was declaring, so now
// it cannot be left undeclared.
func TestAGatedOpMustDeclareItsScope(t *testing.T) {
	r := ops.NewRegistry()
	err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "nameless.scope", Caps: "a", Kind: ops.Command, Auth: ops.Token,
		Route: "POST /api/v1/somewhere", Doc: "an op with letters and no subject",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	})
	if err == nil {
		t.Fatal("an op with Caps, no Scope and no {repo} in its route registered anyway: " +
			"nothing forces the question to be answered")
	}
	for _, want := range []string{"ScopeRepo", "ScopeFleet", "ScopeCaller"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("the refusal does not offer %s, so it does not say how to fix it: %v", want, err)
		}
	}
}

// An op whose route names {repo} needs no declaration: the router always
// has a repository there, so the subject cannot go missing.
func TestARouteNamingTheRepoNeedsNoScope(t *testing.T) {
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "scoped.by.route", Caps: "a", Kind: ops.Query, Auth: ops.Token,
		Route: "GET /api/v1/r/{repo}/thing", Doc: "the route carries the subject",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatalf("an op whose route names {repo} was refused: %v", err)
	}
}

// THE INCIDENT, as a test.
//
// A repo-scoped op reached with no repository must fail LOUDLY as a
// forge fault, not quietly as a capability refusal. The whole cost of
// b683c54b was that the wrong answer was indistinguishable from a right
// one: account.explain said the caller held "a" while repo.update said
// they did not, and both were reporting honestly about different
// subjects.
func TestARepoScopedOpWithNoRepositoryIsAForgeFault(t *testing.T) {
	type in struct {
		Name string `json:"name"`
	}
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[in, struct{}]{
		Name: "body.scoped", Caps: "a", Scope: ops.ScopeRepo, Kind: ops.Command, Auth: ops.Token,
		Route: "POST /api/v1/things/update", Doc: "names its repository in the body",
		Run: func(context.Context, in) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "test", func(_, repo string) CapSet {
		if repo == "sandbox" {
			return oltp.CapSet("a")
		}
		return oltp.CapSet("")
	}, nil)
	if err != nil {
		t.Fatal(err)
	}

	// A body that names no repository at all: the subject is missing.
	body, _ := json.Marshal(in{})
	req := httptest.NewRequest("POST", "/api/v1/things/update", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = req.WithContext(ops.WithUser(req.Context(), "jva"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)

	if w.Code == http.StatusForbidden {
		t.Fatalf("answered 403: a missing subject was reported as a capability refusal, "+
			"which is the disguise that cost two days: %s", w.Body.String())
	}
	if w.Code != http.StatusInternalServerError {
		t.Fatalf("answered %d, want 500 — a subject the forge failed to resolve is the "+
			"forge's fault: %s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "fault in the forge") {
		t.Errorf("the message does not tell the reader it is not their credentials: %s", w.Body.String())
	}

	// And the ordinary case still works: subject present, caps held.
	ok, _ := json.Marshal(in{Name: "sandbox"})
	req2 := httptest.NewRequest("POST", "/api/v1/things/update", bytes.NewReader(ok))
	req2.Header.Set("Content-Type", "application/json")
	req2 = req2.WithContext(ops.WithUser(req2.Context(), "jva"))
	w2 := httptest.NewRecorder()
	h.ServeHTTP(w2, req2)
	if w2.Code != http.StatusOK {
		t.Errorf("a request that DOES name its repository was refused: %d %s", w2.Code, w2.Body.String())
	}
}

// Fleet- and caller-scoped ops are untouched: they never had a
// repository and are not expected to grow one.
func TestFleetScopedOpsAreUnaffected(t *testing.T) {
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "fleetwide", Caps: "s", Scope: ops.ScopeFleet, Kind: ops.Query, Auth: ops.Token,
		Route: "GET /api/v1/fleetwide", Doc: "about the forge",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "test", func(_, _ string) CapSet { return oltp.CapSet("s") }, nil)
	if err != nil {
		t.Fatal(err)
	}
	req := httptest.NewRequest("GET", "/api/v1/fleetwide", nil)
	req = req.WithContext(ops.WithUser(req.Context(), "root"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Errorf("a fleet-scoped op answered %d: %s", w.Code, w.Body.String())
	}
}

// THE OTHER HALF OF THE SCOPE RULE: a caller-scoped op cannot carry
// letters, because there is no repository to resolve them against.
//
// ScopeCaller means "about the signed-in account across every
// repository". The router resolves a letter against the op's
// repository, a caller-scoped op has none, so it resolved to "" and
// answered with the FLEET — where an ordinary account holds nothing a
// repository granted it. Every op that declared one was refused for
// every ordinary account: me.feed, the saved views, the cross-repository
// ticket listing, mirrors. Refused at REGISTRATION so the next one
// cannot reach production, which is the same reason the rule above it
// exists — 7ddd1768 was found one refusal at a time.
func TestACallerScopedOpCannotBeGatedByLetters(t *testing.T) {
	r := ops.NewRegistry()
	err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "caller.letters", Caps: "p", Scope: ops.ScopeCaller, Kind: ops.Query, Auth: ops.Token,
		Route: "GET /api/v1/me/something", Doc: "caller-scoped, and gated by a letter",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	})
	if err == nil {
		t.Fatal("a caller-scoped op registered while declaring a Caps letter: nothing stops the next " +
			"one refusing every ordinary account")
	}
	// The refusal must say how to fix it, and where to read about it.
	for _, want := range []string{"ScopeCaller", "Auth", "7ddd1768"} {
		if !strings.Contains(err.Error(), want) {
			t.Errorf("the refusal does not mention %q, so it does not say how to fix it: %v", want, err)
		}
	}
}

// And a caller-scoped op WITHOUT letters registers and is served, which
// is the shape every one of them now has: Auth is the gate, and the Run
// body filters by reach.
func TestACallerScopedOpWithoutLettersIsServed(t *testing.T) {
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "caller.clean", Scope: ops.ScopeCaller, Kind: ops.Query, Auth: ops.Token,
		Route: "GET /api/v1/me/thing", Doc: "caller-scoped, gated by being signed in",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatalf("a caller-scoped op with no letters was refused: %v", err)
	}
	h, err := RouterWithCaps(r, "test", func(_, _ string) CapSet { return oltp.CapSet("") }, nil)
	if err != nil {
		t.Fatal(err)
	}
	// An ORDINARY account: signed in, holding nothing anywhere. This is
	// the account the old gate refused.
	req := httptest.NewRequest("GET", "/api/v1/me/thing", nil)
	req = req.WithContext(ops.WithUser(req.Context(), "ordinary"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)
	if w.Code != http.StatusOK {
		t.Errorf("a signed-in ordinary account was refused %d from its own caller-scoped op: %s",
			w.Code, w.Body.String())
	}
}

// A refusal with NO repository names a SITE ADMIN, not a repository's.
//
// GET /api/v1/mirrors is a fleet route; it told callers their missing
// letter was something a repository's admin could hand them, and named a
// repository in a sentence about the forge — "a fleet-level list
// refusing with a per-repo settings error". Asking the wrong person for
// a letter is a dead end dressed as instructions.
func TestAFleetRefusalNamesASiteAdminNotARepository(t *testing.T) {
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[struct{}, struct{}]{
		Name: "fleet.gated", Caps: "a", Scope: ops.ScopeFleet, Kind: ops.Query, Auth: ops.Token,
		Route: "GET /api/v1/fleetwide-thing", Doc: "a fleet question with letters",
		Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
	}); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "test", func(_, _ string) CapSet { return oltp.CapSet("") }, nil)
	if err != nil {
		t.Fatal(err)
	}
	req := httptest.NewRequest("GET", "/api/v1/fleetwide-thing", nil)
	req = req.WithContext(ops.WithUser(req.Context(), "jva"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)
	if w.Code != http.StatusForbidden {
		t.Fatalf("a fleet-scoped refusal answered %d, not 403: %s", w.Code, w.Body.String())
	}
	body := w.Body.String()
	if !strings.Contains(body, "a site admin") {
		t.Errorf("the refusal does not name the person who can grant it: %s", body)
	}
	if strings.Contains(body, "admin of the repository") {
		t.Errorf("a fleet refusal names a REPOSITORY's admin, which is a grantor who cannot grant it: %s", body)
	}
}