~bigbes/sr-ht-spec

ref: 53e56db27ab35117d0c2a91f15533f7dd528612c sr-ht-spec/web/grant_test.go -rw-r--r-- 4.6 KiB
53e56db2 — Eugene Blikh web: draw the chrome from sr-ht-ecore 9 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
120
121
122
123
124
125
126
127
128
129
130
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)
}