Files
summercms/modules/cabana/permissions_test.go

144 lines
6.4 KiB
Go

package cabana
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestAllowsFollowsWinterHasAnyAccess pins the permission check to Winter's
// User::hasAnyAccess: wildcards match on both sides and several required codes
// are an OR.
func TestAllowsFollowsWinterHasAnyAccess(t *testing.T) {
grant := func(codes ...string) *bouncer.Principal {
grants := map[string]bool{}
for _, code := range codes {
grants[code] = true
}
return &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: grants}
}
tests := []struct {
name string
principal *bouncer.Principal
required []string
want bool
}{
{"nil principal", nil, []string{"a.b.c"}, false},
{"superuser", &bouncer.Principal{Backend: true, IsSuperuser: true}, []string{"a.b.c"}, true},
{"empty requirement is open", grant(), nil, true},
{"exact grant", grant("a.b.c"), []string{"a.b.c"}, true},
{"missing grant", grant("a.b.d"), []string{"a.b.c"}, false},
{"grant wildcard covers code", grant("a.b.*"), []string{"a.b.c"}, true},
{"grant wildcard other prefix", grant("a.x.*"), []string{"a.b.c"}, false},
{"required wildcard met by any grant under the prefix", grant("a.b.access_genres"), []string{"a.b.*"}, true},
{"required wildcard not met by a sibling plugin", grant("a.x.access_genres"), []string{"a.b.*"}, false},
{"required wildcard with no grants", grant(), []string{"a.b.*"}, false},
{"required wildcard met by a grant wildcard", grant("a.b.*"), []string{"a.b.*"}, true},
{"required leading wildcard", grant("a.b.access_genres"), []string{"*.access_genres"}, true},
{"required leading wildcard miss", grant("a.b.access_styles"), []string{"*.access_genres"}, false},
{"several codes are any, first grants", grant("a.b.one"), []string{"a.b.one", "a.b.two"}, true},
{"several codes are any, last grants", grant("a.b.two"), []string{"a.b.one", "a.b.two"}, true},
{"several codes are any, none grants", grant("a.b.three"), []string{"a.b.one", "a.b.two"}, false},
{"disabled grant is not a grant", &bouncer.Principal{PermissionGrants: map[string]bool{"a.b.c": false}}, []string{"a.b.c"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Allows(tt.principal, tt.required); got != tt.want {
t.Fatalf("Allows(%v) = %v, want %v", tt.required, got, tt.want)
}
})
}
}
type navController struct {
id string
required []string
}
func (c navController) ID() string { return c.id }
func (navController) ModelName() string { return "Metadata" }
func (navController) ConfigDir() string { return "controllers/metadata" }
func (c navController) RequiredPermissions() []string { return c.required }
// TestNavigationDropsDeniedParentAndRepointsTarget covers the WR-02 rules: a
// main item the principal may not open is dropped whatever its children allow,
// and an allowed parent never links to a controller the principal cannot open.
func TestNavigationDropsDeniedParentAndRepointsTarget(t *testing.T) {
reg := &Registry{
byID: map[string]*CompiledController{
"acme.shop.albums": {Controller: navController{"acme.shop.albums", []string{"acme.shop.access_albums"}}},
"acme.shop.genres": {Controller: navController{"acme.shop.genres", []string{"acme.shop.access_genres"}}},
},
navigation: []pact.NavigationItem{
{
Code: "shop", Label: "Shop", Controller: "acme.shop.albums", Permissions: []string{"acme.shop.*"},
SideMenu: []pact.NavigationItem{
{Code: "albums", Label: "Albums", Controller: "acme.shop.albums", Permissions: []string{"acme.shop.access_albums"}},
{Code: "genres", Label: "Genres", Controller: "acme.shop.genres", Permissions: []string{"acme.shop.access_genres"}},
},
},
{
Code: "locked", Label: "Locked", Controller: "acme.shop.albums", Permissions: []string{"acme.locked.access"},
SideMenu: []pact.NavigationItem{
{Code: "genres", Label: "Genres", Controller: "acme.shop.genres", Permissions: []string{"acme.shop.access_genres"}},
},
},
},
}
genresOnly := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"acme.shop.access_genres": true}}
nav, _ := reg.Metadata(context.Background(), genresOnly, nil)
if len(nav) != 1 || nav[0].Code != "shop" {
t.Fatalf("navigation = %#v, want only the shop item (the locked parent must be dropped)", nav)
}
if nav[0].Controller != "acme.shop.genres" {
t.Fatalf("parent controller = %q, want the first openable child", nav[0].Controller)
}
if len(nav[0].SideMenu) != 1 || nav[0].SideMenu[0].Code != "genres" {
t.Fatalf("side menu = %#v", nav[0].SideMenu)
}
both := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{"acme.shop.access_albums": true}}
nav, _ = reg.Metadata(context.Background(), both, nil)
if len(nav) != 1 || nav[0].Controller != "acme.shop.albums" {
t.Fatalf("navigation = %#v, want the parent to keep its own controller", nav)
}
}
// TestRelationMutationsFollowToolbarButtons pins WR-05: link and unlink are
// refused unless the relation's view panel declares them.
func TestRelationMutationsFollowToolbarButtons(t *testing.T) {
svc := phase09DeniedService()
cc := svc.reg.byID["acme.demo.widgets"]
super := &bouncer.Principal{ID: 1, Backend: true, IsSuperuser: true}
relation := func(buttons ...string) {
cc.Relations = map[string]*CompiledRelation{"editors": {Schema: &RelationSchema{Name: "editors", View: RelationPanel{ToolbarButtons: buttons}}}}
}
call := func(handler func(*service, http.ResponseWriter, *http.Request)) int {
rec := httptest.NewRecorder()
handler(svc, rec, phase09Request(super))
return rec.Code
}
relation("link")
if code := call((*service).relationUnlink); code != http.StatusForbidden {
t.Fatalf("unlink on a link-only relation = %d, want 403", code)
}
if code := call((*service).relationLink); code == http.StatusForbidden {
t.Fatal("link on a link-only relation was refused")
}
relation()
for name, handler := range map[string]func(*service, http.ResponseWriter, *http.Request){"link": (*service).relationLink, "unlink": (*service).relationUnlink} {
if code := call(handler); code != http.StatusForbidden {
t.Fatalf("%s on a relation with no buttons = %d, want 403", name, code)
}
}
relation("link", "unlink")
if code := call((*service).relationUnlink); code == http.StatusForbidden {
t.Fatal("unlink on a link|unlink relation was refused")
}
}