package lua // Plugin visibility: a plugin page is not automatically public data. // // The billing page renders GLOBAL cost across every source and the quota page // renders upstream balances. Both were served to any authenticated key because // /api/ui-inject applied no role filter — measured, not theorised: a user-role // key pulled the 18KB billing state and read the source names and the $8.86 // total straight out of it. // // These tests pin the DENIALS. A visibility feature whose tests only assert that // an admin still sees everything passes while the leak is wide open. import ( "testing" ) func TestAudienceAllows(t *testing.T) { cases := []struct { want, role string ok bool why string }{ {"", "user", true, "unset means everyone"}, {"all", "user", true, "explicit all"}, {"admin", "admin", true, "exact match"}, {"admin", "user", false, "admin-only excludes user"}, {"user", "user", true, "user-only admits user"}, {"user", "admin", false, "user-only excludes admin"}, {"admin,user", "user", true, "list admits a listed role"}, {" admin , user ", "user", true, "whitespace is not significant"}, {"ADMIN", "user", false, "case is normalised"}, {"*", "user", false, `"*" must NOT be a synonym for all`}, } for _, c := range cases { if got := audienceAllows(c.want, c.role); got != c.ok { t.Errorf("audienceAllows(%q, %q) = %v, want %v (%s)", c.want, c.role, got, c.ok, c.why) } } } func TestPluginVisibilityHidesPageFromUserRole(t *testing.T) { dir := t.TempDir() ps := NewPlugins(NewVM(dir), dir) // One admin-only plugin, one open plugin: the filter must drop exactly one // and leave the other intact, or a plugin author has no way to ship a mixed // set. const adminOnly = ` local P = {} P.name = "adminonly" P.version = "1.0.0" P.exports = {} P.ui = { visibility = "admin", page = { page_id = "secret", title = "Secret", mount = "
secret
" } } return P ` const open = ` local P = {} P.name = "openpage" P.version = "1.0.0" P.exports = {} P.ui = { page = { page_id = "open", title = "Open", mount = "
open
" } } return P ` for name, src := range map[string]string{"adminonly": adminOnly, "openpage": open} { if err := ps.LoadSource(name, src); err != nil { t.Fatalf("load %s: %v", name, err) } } admin := ps.UIForRole("admin") if len(admin.Pages) != 2 { t.Errorf("admin should see both pages, got %d", len(admin.Pages)) } user := ps.UIForRole("user") for _, pg := range user.Pages { if pg.PageID == "secret" { t.Fatal("a user key can see the admin-only page — this is the leak") } } if len(user.Pages) != 1 || user.Pages[0].PageID != "open" { t.Fatalf("user should see exactly the open page, got %v", pageIDs(user.Pages)) } } // The operator must be able to impose visibility the plugin did not choose — // that is the whole point of the override, since a bundled .lua cannot be edited // without being overwritten on the next upgrade. func TestPluginVisibilityOverrideBeatsDeclaration(t *testing.T) { dir := t.TempDir() ps := NewPlugins(NewVM(dir), dir) const open = ` local P = {} P.name = "openpage" P.version = "1.0.0" P.exports = {} P.ui = { page = { page_id = "open", title = "Open", mount = "
open
" } } return P ` if err := ps.LoadSource("openpage", open); err != nil { t.Fatal(err) } if len(ps.UIForRole("user").Pages) != 1 { t.Fatal("precondition: the page should start visible to user") } ps.SetPluginVisibility("openpage", "admin") if got := ps.UIForRole("user").Pages; len(got) != 0 { t.Fatalf("the operator override was ignored: user still sees %v", pageIDs(got)) } if ps.VisibilityOverride("openpage") != "admin" { t.Error("the override should be reportable so /api/plugins can show it") } // Clearing restores the plugin's own declaration. ps.SetPluginVisibility("openpage", "") if got := ps.UIForRole("user").Pages; len(got) != 1 { t.Fatalf("clearing the override should restore visibility, got %v", pageIDs(got)) } } // A page may narrow visibility below the plugin default, so one plugin can ship // a public summary beside an admin-only detail page. func TestPluginPageCanNarrowBelowPluginDefault(t *testing.T) { dir := t.TempDir() ps := NewPlugins(NewVM(dir), dir) const mixed = ` local P = {} P.name = "mixed" P.version = "1.0.0" P.exports = {} P.ui = { pages = { { page_id = "summary", title = "Summary", mount = "
s
" }, { page_id = "details", title = "Details", mount = "
d
", audience = "admin" }, }, } return P ` if err := ps.LoadSource("mixed", mixed); err != nil { t.Fatal(err) } user := ps.UIForRole("user") if len(user.Pages) != 1 || user.Pages[0].PageID != "summary" { t.Fatalf("user should see only the summary, got %v", pageIDs(user.Pages)) } if len(ps.UIForRole("admin").Pages) != 2 { t.Error("admin should see both pages") } } func TestPluginStateVisibleTo(t *testing.T) { dir := t.TempDir() ps := NewPlugins(NewVM(dir), dir) const adminOnly = ` local P = {} P.name = "adminonly" P.version = "1.0.0" P.exports = {} P.state = { secret = "x" } P.ui = { visibility = "admin", page = { page_id = "s", title = "S", mount = "
" } } return P ` if err := ps.LoadSource("adminonly", adminOnly); err != nil { t.Fatal(err) } // The data has to follow the page: a hidden UI with an open state endpoint is // the same leak with one more step. if ps.StateVisibleTo("adminonly", "user") { t.Error("user must not read an admin-only plugin's state") } if !ps.StateVisibleTo("adminonly", "admin") { t.Error("admin must still read it") } // Unknown plugin: unchanged behaviour, so a widget that is not installed does // not start answering 403. if !ps.StateVisibleTo("nosuchplugin", "user") { t.Error("an unknown plugin name must not become forbidden; that would change " + "behaviour for plugins that simply are not installed") } } func pageIDs(pages []*UIPage) []string { out := make([]string, 0, len(pages)) for _, p := range pages { if p != nil { out = append(out, p.PageID) } } return out }