package web
import (
"os"
"regexp"
"strings"
"testing"
)
// The fleet is an administrator's view of the whole forge, and every
// route under it is supposed to be admin-only. /fleet was. /fleet.frag
// was not, and it renders the same panel — so a signed-out stranger
// could read fifty kilobytes of the fleet page from the fragment while
// the page itself answered 303 to the login form.
//
// This is the third door-with-a-weaker-lock of its kind: an API gate
// that disagreed with the web gate, a sync path that never consulted
// the letters the page promised, and now a fragment that skipped the
// gate its page carries. Each was found by hand, one at a time, after
// shipping. The pattern underneath is always the same — a SECOND way
// in that nobody remembered to lock, because the lock lives in the
// handler rather than in the route.
//
// So this test refuses the class rather than the instance: a handler
// registered under /fleet must gate, whatever it is called and whoever
// adds it next. A source-level check rather than a request-level one
// because it has to hold for routes that do not exist yet — the point
// is to fail the day somebody adds the fourth door, not to re-prove
// the three we know about.
func TestFleetRoutesAreAdminGated(t *testing.T) {
src, err := os.ReadFile("web.go")
if err != nil {
t.Fatal(err)
}
// Each registration and the body that follows it, up to the next
// one. Good enough to see whether the gate is the first thing the
// handler does, which is the only place it is any use.
// EVERY registration, so a handler's body is bounded by the next
// route of any path. Bounding by the next /fleet route instead let
// the LAST one run to end-of-file and swallow some unrelated
// handler's gateAt — the guard passed while the hole was open,
// which is the same shape of mistake it exists to catch.
any := regexp.MustCompile(`mux\.Handle(?:Func)?\("(?:GET|POST) ([^"]*)"`)
all := any.FindAllSubmatchIndex(src, -1)
if len(all) == 0 {
t.Fatal("no routes found — this test has lost track of the file it guards")
}
var ungated []string
for i, loc := range all {
route := string(src[loc[2]:loc[3]])
if !strings.HasPrefix(route, "/fleet") {
continue
}
end := len(src)
if i+1 < len(all) {
end = all[i+1][0]
}
body := string(src[loc[1]:end])
if !strings.Contains(body, "gateAt(") {
ungated = append(ungated, route)
}
}
if len(ungated) > 0 {
t.Errorf("these /fleet routes do not gate, so a signed-out stranger reaches them:\n %s\n"+
"every route under /fleet is an admin view; add gateAt(verify, sessions, \"\", ops.Admin, w, req)",
strings.Join(ungated, "\n "))
}
}
// The two that were actually open, named so a regression on either is
// reported as itself rather than as a count.
func TestFleetFragmentsGateLikeTheirPage(t *testing.T) {
src, err := os.ReadFile("web.go")
if err != nil {
t.Fatal(err)
}
for _, route := range []string{"/fleet.frag", "/fleet.rev"} {
i := strings.Index(string(src), `mux.HandleFunc("GET `+route+`"`)
if i < 0 {
t.Errorf("%s is gone — if it moved, move this guard with it", route)
continue
}
rest := string(src[i:])
if end := strings.Index(rest[1:], "mux.HandleFunc("); end >= 0 {
rest = rest[:end]
}
if !strings.Contains(rest, "ops.Admin") {
t.Errorf("%s does not gate on ops.Admin: it renders the /fleet panel and must be as hard to reach as /fleet", route)
}
}
}