~bigbes/sr-ht-spec

ref: 824788ab8269bd6c58de5848bc4545450fac7aaf sr-ht-spec/web/grant_test.go -rw-r--r-- 4.2 KiB
824788ab — Eugene Blikh mcpsrv: mark /mcp uncacheable, fail closed on origin, split tool errors from faults 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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"

	"sourcecraft.dev/bigbes/sr-ht-ecore/ecoretest"
	"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:     ecoretest.Config(authn.ConfigSection),
		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"), ecoretest.Origin("meta.sr.ht"))

	rec = httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/~bigbes/rfcs/specs/0007-storage.md", nil))
	assert.Equal(t, http.StatusUnauthorized, rec.Code)
}