fix(09): WR-02 drop a denied main menu item and never link it to a controller the admin cannot open

This commit is contained in:
Jakub Zych
2026-10-01 20:59:26 +02:00
parent b4b8b5df64
commit 28aa073de0
3 changed files with 93 additions and 5 deletions

View File

@@ -40,6 +40,12 @@ func (r *Registry) Metadata(ctx context.Context, principal *bouncer.Principal, t
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) {
@@ -47,10 +53,9 @@ func (r *Registry) Metadata(ctx context.Context, principal *bouncer.Principal, t
}
children = append(children, navigationView(ctx, tr, child, nil))
}
if !Allows(principal, item.Permissions) && len(children) == 0 {
continue
}
navigation = append(navigation, navigationView(ctx, tr, item, children))
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 {
@@ -83,6 +88,33 @@ func (r *Registry) Metadata(ctx context.Context, principal *bouncer.Principal, t
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{}