This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 01e304d999a17d263acde87b05ea4499e89a646a Author: Wu Sheng <[email protected]> AuthorDate: Wed May 20 16:57:01 2026 +0800 ui: RBAC-gate the sidebar menu by read verb Most static menus had no verb and always showed. Tag each with the read verb its page's primary data route requires (route-policy.ts), so the sidebar hides what the user can't read (BFF still enforces server-side): Operate: Cluster status → cluster:read · Alerting rules → alarm-rule:read · Live debugger + Capture history → live-debug:read · Metrics Inspect → inspect:read · DSL Management (+children) → rule:read Dashboard setup: Overview templates → overview:read · Layer dashboards → dashboard:read · Alert page → alarm-setup:read · Global defaults → setup:read Admin: unchanged (user/auth/role :read) Dynamic menus: Overviews gated by overview:read, Alarms by alarms:read. Layers stay visible to any read role (cap-gated already). --- apps/ui/src/shell/AppSidebar.vue | 63 +++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/apps/ui/src/shell/AppSidebar.vue b/apps/ui/src/shell/AppSidebar.vue index c412944..22b3695 100644 --- a/apps/ui/src/shell/AppSidebar.vue +++ b/apps/ui/src/shell/AppSidebar.vue @@ -171,36 +171,41 @@ interface NavSection { links: NavRow[]; } +// Each static menu carries the read verb its page's primary data route +// requires (see apps/bff/src/rbac/route-policy.ts). The sidebar removes +// rows the user can't read; the BFF enforces the same verbs server-side. const sections: NavSection[] = [ { kicker: 'Operate', links: [ - { icon: 'svc', label: 'Cluster status', to: '/operate/cluster' }, - { icon: 'alert', label: 'Alerting rules', to: '/operate/alerting-rules' }, + { icon: 'svc', label: 'Cluster status', to: '/operate/cluster', verb: 'cluster:read' }, + { icon: 'alert', label: 'Alerting rules', to: '/operate/alerting-rules', verb: 'alarm-rule:read' }, { icon: 'flame', label: 'Live debugger', to: '/operate/live-debug', + verb: 'live-debug:read', // Match the tab variants only; the history sibling at // /operate/live-debug/history must NOT highlight this row. activeWhen: (p) => p === '/operate/live-debug' || /^\/operate\/live-debug\/(mal|lal|oal)(\/|$)/.test(p), }, - { icon: 'event', label: 'Capture history', to: '/operate/live-debug/history' }, - { icon: 'metric', label: 'Metrics Inspect', to: '/operate/inspect' }, + { icon: 'event', label: 'Capture history', to: '/operate/live-debug/history', verb: 'live-debug:read' }, + { icon: 'metric', label: 'Metrics Inspect', to: '/operate/inspect', verb: 'inspect:read' }, { icon: 'set', label: 'DSL Management', // No standalone landing — `to` jumps to the first rule page so // the L1 itself is clickable; activeWhen covers all DSL routes. to: '/operate/dsl/otel-rules', + verb: 'rule:read', activeWhen: (p) => p === '/operate/oal' || /^\/operate\/dsl(\/|$)/.test(p), children: [ - { icon: 'set', label: 'MAL · OTEL', to: '/operate/dsl/otel-rules' }, - { icon: 'set', label: 'MAL · Telegraf', to: '/operate/dsl/telegraf-rules' }, - { icon: 'set', label: 'LAL', to: '/operate/dsl/lal' }, - { icon: 'set', label: 'LAL → MAL', to: '/operate/dsl/log-mal-rules' }, - { icon: 'trace', label: 'OAL · read-only', to: '/operate/oal' }, - { icon: 'download', label: 'Dump & restore', to: '/operate/dsl/dump' }, + { icon: 'set', label: 'MAL · OTEL', to: '/operate/dsl/otel-rules', verb: 'rule:read' }, + { icon: 'set', label: 'MAL · Telegraf', to: '/operate/dsl/telegraf-rules', verb: 'rule:read' }, + { icon: 'set', label: 'LAL', to: '/operate/dsl/lal', verb: 'rule:read' }, + { icon: 'set', label: 'LAL → MAL', to: '/operate/dsl/log-mal-rules', verb: 'rule:read' }, + { icon: 'trace', label: 'OAL · read-only', to: '/operate/oal', verb: 'rule:read' }, + { icon: 'download', label: 'Dump & restore', to: '/operate/dsl/dump', verb: 'rule:read' }, ], }, ], @@ -208,10 +213,10 @@ const sections: NavSection[] = [ { kicker: 'Dashboard setup', links: [ - { icon: 'set', label: 'Overview templates', to: '/admin/overview-templates' }, - { icon: 'metric', label: 'Layer dashboards', to: '/admin/layer-dashboards' }, - { icon: 'alert', label: 'Alert page', to: '/admin/alert-page-setup' }, - { icon: 'set', label: 'Global defaults', to: '/admin/global-defaults' }, + { icon: 'set', label: 'Overview templates', to: '/admin/overview-templates', verb: 'overview:read' }, + { icon: 'metric', label: 'Layer dashboards', to: '/admin/layer-dashboards', verb: 'dashboard:read' }, + { icon: 'alert', label: 'Alert page', to: '/admin/alert-page-setup', verb: 'alarm-setup:read' }, + { icon: 'set', label: 'Global defaults', to: '/admin/global-defaults', verb: 'setup:read' }, ], }, { @@ -280,20 +285,24 @@ watch( </RouterLink> <nav class="sw-nav"> - <div class="sw-nav-section sw-nav-section--icon"> - <Icon :name="sectionIcon('Overviews')" /> - <span>Overviews</span> - </div> - <RouterLink - v-for="ov in publicOverviews" - :key="`pub:${ov.id}`" - :to="`/overview/${ov.id}`" - class="sw-nav-item" - :class="{ 'is-active': isActive(`/overview/${ov.id}`) }" - > - <Icon :name="(ov.icon as IconName) || 'dash'" /><span>{{ ov.title }}</span> - </RouterLink> + <!-- Overviews are gated by `overview:read` (operator / admin). --> + <template v-if="auth.hasVerb('overview:read')"> + <div class="sw-nav-section sw-nav-section--icon"> + <Icon :name="sectionIcon('Overviews')" /> + <span>Overviews</span> + </div> + <RouterLink + v-for="ov in publicOverviews" + :key="`pub:${ov.id}`" + :to="`/overview/${ov.id}`" + class="sw-nav-item" + :class="{ 'is-active': isActive(`/overview/${ov.id}`) }" + > + <Icon :name="(ov.icon as IconName) || 'dash'" /><span>{{ ov.title }}</span> + </RouterLink> + </template> <RouterLink + v-if="auth.hasVerb('alarms:read')" to="/alarms" class="sw-nav-item" :class="{ 'is-active': isActive('/alarms') }"
