144 lines
6.4 KiB
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")
|
|
}
|
|
}
|