package cabana import ( "context" "sort" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/phrasebook" ) // NavigationEntry is one permission-filtered backend navigation item. type NavigationEntry struct { Code string `json:"code"` Label string `json:"label"` Icon string `json:"icon"` Order int `json:"order"` Controller string `json:"controller"` SideMenu []NavigationEntry `json:"sideMenu"` } // SettingsEntry is one permission-filtered settings-list item. type SettingsEntry struct { Code string `json:"code"` Label string `json:"label"` Description string `json:"description"` Category string `json:"category"` Icon string `json:"icon"` Order int `json:"order"` Keywords []string `json:"keywords"` Model string `json:"model"` } // Metadata returns only entries the backend principal may open. Denied // entries are removed before any response value is constructed. func (r *Registry) Metadata(ctx context.Context, principal *bouncer.Principal, tr *phrasebook.Translator) ([]NavigationEntry, []SettingsEntry) { navigation := make([]NavigationEntry, 0) settings := make([]SettingsEntry, 0) if r == nil || principal == nil || !principal.Backend { return navigation, settings } for _, item := range r.navigation { // Like Winter's NavigationManager, a main item the principal may not // open is dropped whatever its children allow, so a denied parent // never leaks its label or target controller. if !Allows(principal, item.Permissions) { continue } children := make([]NavigationEntry, 0) for _, child := range item.SideMenu { if !Allows(principal, child.Permissions) { continue } children = append(children, navigationView(ctx, tr, child, nil)) } view := navigationView(ctx, tr, item, children) view.Controller = r.openableTarget(principal, item, children) navigation = append(navigation, view) } sort.SliceStable(navigation, func(i, j int) bool { if navigation[i].Order == navigation[j].Order { return navigation[i].Code < navigation[j].Code } return navigation[i].Order < navigation[j].Order }) for _, compiled := range r.settings { item := compiled.Item if !Allows(principal, item.Permissions) { continue } keywords := append([]string(nil), item.Keywords...) if keywords == nil { keywords = []string{} } settings = append(settings, SettingsEntry{ Code: item.Code, Label: translateKey(ctx, tr, item.Label), Description: translateKey(ctx, tr, item.Description), Category: translateKey(ctx, tr, item.Category), Icon: item.Icon, Order: item.Order, Keywords: keywords, Model: item.Model, }) } sort.SliceStable(settings, func(i, j int) bool { if settings[i].Order == settings[j].Order { return settings[i].Code < settings[j].Code } return settings[i].Order < settings[j].Order }) return navigation, settings } // openableTarget returns the controller a main item should link to. It is the // item's own controller unless the principal cannot open it, in which case it // is the first side-menu entry the principal can, so the menu never links to a // page that answers 403. It is empty when nothing is openable. func (r *Registry) openableTarget(principal *bouncer.Principal, item pact.NavigationItem, children []NavigationEntry) string { if item.Controller == "" || r.canOpen(principal, item.Controller) { return item.Controller } for _, child := range children { if child.Controller != "" && r.canOpen(principal, child.Controller) { return child.Controller } } return "" } // canOpen reports whether principal passes a registered controller's required // permissions. A controller the registry does not know is not blocked here: // the guard answers for it when it is opened. func (r *Registry) canOpen(principal *bouncer.Principal, controller string) bool { cc, ok := r.Get(controller) if !ok { return true } return Allows(principal, requiredOf(cc.Controller)) } func navigationView(ctx context.Context, tr *phrasebook.Translator, item pact.NavigationItem, children []NavigationEntry) NavigationEntry { if children == nil { children = []NavigationEntry{} } return NavigationEntry{ Code: item.Code, Label: translateKey(ctx, tr, item.Label), Icon: item.Icon, Order: item.Order, Controller: item.Controller, SideMenu: children, } }