package web import ( "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vaughan0/go-ini" "sourcecraft.dev/bigbes/sr-ht-ecore/grants" "sourcecraft.dev/bigbes/sr-ht-spec/authn" ) // mustGrants parses a grant string or fails the test. func mustGrants(t *testing.T, s string) grants.Grants { t.Helper() g, err := grants.Parse(s) require.NoError(t, err, "parse grants %q", s) return g } // grantRouter mounts the read plane with a fixed principal injected into every // request, bypassing token resolution: what put the principal there is the // resolver's business, and this file is about what the handlers do with it. func grantRouter(t *testing.T, p authn.Principal) http.Handler { t.Helper() srv, err := New(Options{ Conf: ini.File{ "sr.ht": ini.Section{ "network-key": testConf.Section("sr.ht")["network-key"], "site-name": "sourcehut", "environment": "development", "owner-name": "bigbes", }, "webhooks": ini.Section{"private-key": testConf.Section("webhooks")["private-key"]}, "spec.sr.ht": ini.Section{"origin": "https://spec.example"}, "meta.sr.ht": ini.Section{"origin": "https://meta.example"}, "tokens.sr.ht": ini.Section{"origin": "https://tokens.example"}, }, Reader: newFakeReader(), Searcher: &fakeSearcher{}, Resolver: testResolver(t), }) require.NoError(t, err) r := chi.NewRouter() r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { next.ServeHTTP(w, req.WithContext(authn.WithPrincipal(req.Context(), p))) }) }) srv.Register(r) return r } func instancePrincipal(t *testing.T, grantString string) authn.Principal { t.Helper() return authn.Principal{ Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude-code", Session: "s-1", Plane: authn.PlaneInstance, Grants: mustGrants(t, grantString), } } // Every read route asks for spec:read from a tokens.sr.ht working token, and // asks nothing extra of the credentials that carry no grants. The refusal is a // 403 and pointedly not a login redirect: the caller is already authenticated, // so sending it to meta would loop it back with the same token. func TestReadRoutesRequireTheReadGrant(t *testing.T) { targets := []string{ "/~bigbes/rfcs", "/~bigbes/rfcs/specs/0007-storage", "/~bigbes/rfcs/specs/0007-storage.md", "/~bigbes/rfcs/specs/0007-storage.json", "/search?q=storage", "/inbox", } t.Run("refused without it", func(t *testing.T) { h := grantRouter(t, instancePrincipal(t, "spec:propose")) for _, target := range targets { t.Run(target, func(t *testing.T) { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil)) assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body) assert.Contains(t, rec.Body.String(), authn.ActionRead) assert.Empty(t, rec.Header().Get("Location"), "an authenticated caller must not be redirected to a login") }) } }) // Everything that may read: the owner's cookie, an agent a local process // asserted (no credential, so no grant to read), and the instance tokens // minted for reading. All served, no 403 anywhere. for name, p := range map[string]authn.Principal{ "owner cookie": {Kind: authn.KindOwner, Owner: "bigbes", CookieUser: "bigbes"}, "locally asserted agent": {Kind: authn.KindAgent, Owner: "bigbes"}, "instance token, spec:read": instancePrincipal(t, "spec:read"), "instance token, universal": instancePrincipal(t, "*"), } { t.Run("served for "+name, func(t *testing.T) { h := grantRouter(t, p) for _, target := range targets { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil)) assert.Equal(t, http.StatusOK, rec.Code, "%s: body %s", target, rec.Body) } }) } } // An anonymous browser is still sent to meta's login and an anonymous bot still // gets a 401: the grant check sits behind the identity one and does not change // what happens when there is no identity at all. func TestAnonymousDenialIsUnchanged(t *testing.T) { h := grantRouter(t, authn.Anonymous()) rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/~bigbes/rfcs/specs/0007-storage", nil)) assert.Equal(t, http.StatusFound, rec.Code) assert.Contains(t, rec.Header().Get("Location"), "meta.example") rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/~bigbes/rfcs/specs/0007-storage.md", nil)) assert.Equal(t, http.StatusUnauthorized, rec.Code) }