Skip to content

Hide tabs based on user authorization levels#3035

Open
WilliamBZA wants to merge 6 commits into
authorizationfrom
show-hide-tabs
Open

Hide tabs based on user authorization levels#3035
WilliamBZA wants to merge 6 commits into
authorizationfrom
show-hide-tabs

Conversation

@WilliamBZA

@WilliamBZA WilliamBZA commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Gates the ServicePulse UI on the set of ServiceControl API routes the signed-in user is actually allowed to call (the my/routes manifest). Navigation items and Configuration sub-tabs the user cannot reach are hidden; action buttons they cannot use are disabled with an explanatory tooltip (not hidden), so the UI stays discoverable.

Gating is fail-open: it only applies once authorization is enabled, the user is authenticated, and the manifest has loaded. In every other state the full UI is shown, so an unreachable or slow ServiceControl never locks a user out.

This PR consolidates the work previously split across #3041 and #3044. It supersedes the earlier permission-string gating model, which was introduced and then removed on this branch in favour of the route-based approach.

How it works

  • Route registry (composables/apiRoutes.ts): maps each gated UI capability to the ServiceControl HTTP route it represents. This is the single point of coupling to ServiceControl's route surface; the UI gates on routes it already calls, never on internal permission names.
  • Structural matching (composables/routeMatching.ts): normalizes a (method, path) pair into a comparison key with route parameters collapsed to {}, so matching couples to method plus path structure and survives server-side parameter renames.
  • Manifest store (stores/AllowedRoutesStore.ts): fetches my/routes from the Primary and Monitoring instances, merges them, and keeps a per-store in-flight guard so concurrent callers share one request. Per-instance fail-open: a failed or non-array response is ignored rather than fataled.
  • Gating primitive (composables/useAllowedRoutes.ts): exposes canCall and canAnyCall. Resource-owning stores call these and surface capability getters; templates bind to those getters.
  • PermissionGate wrapper (components/PermissionGate.vue): reusable component that disables a slotted action and shows a tooltip with the reason when the user lacks the route, instead of removing the control.

What is gated

  • Navigation menu (PageHeader.vue) and Configuration sub-tabs
    (ConfigurationView.vue): hidden when no backing route is allowed.
  • Action buttons, disabled with tooltip: failed messages, deleted messages and message groups, custom-check dismiss, heartbeat endpoint instances, redirects, health-check notifications (manage and test), edit and retry, and monitored endpoint deletion.
  • Resource-owning stores expose capability getters: HealthChecksStore, HeartbeatInstancesStore, MessageStore, MonitoringEndpointDetailsStore, RedirectsStore, ThroughputStore.
  • A new User Permissions configuration page (UserPermissions.vue) renders the capabilities derived from the user's allowed routes.

Design decisions

  • Fail-open everywhere: uncertainty (auth disabled, not yet authenticated, manifest not loaded) shows the UI rather than hiding it.
  • Gate on routes the UI already calls, decoupling the frontend from ServiceControl's internal permission model.
  • Structural, parameter-insensitive route keys so the contract is method plus path shape, not exact parameter names.
  • The manifest is stored as a Map (not a Set) so a future per-route resource scope can be carried per entry.
  • The startup fetch surfaces a 403 denial reason so an explicitly forbidden user gets a clear message.

Backend dependency

Requires ServiceControl to serve the my/routes allowed-route manifest on the
Primary instance, and the equivalent on the Monitoring instance.

Reviewer Checklist

  • Components are broken down into sensible and maintainable sub-components.
  • Styles are scoped to the component using it. If multiple components need to share CSS, then a .css file is created containing the shared CSS and imported into component scoped style sections.
  • Naming is consistent with existing code, and adequately describes the component or function being introduced
  • Only functions utilizing Vue state or lifecycle hooks are named as composables (i.e. starting with 'use');
  • No module-level state is being introduced. If so, request the PR author to move the state to the corresponding Pinia store.

@WilliamBZA WilliamBZA marked this pull request as ready for review June 19, 2026 09:15
@WilliamBZA WilliamBZA changed the title Show hide tabs Hide tabs based on user authorization levels Jun 19, 2026

@ramonsmits ramonsmits left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some improvement on:

dvdstelt added a commit that referenced this pull request Jun 22, 2026
Review fixes for user authorization tab hiding (#3035)
ramonsmits and others added 6 commits June 26, 2026 15:56
Map each gated UI capability to the ServiceControl HTTP route it represents (apiRoutes.ts), and normalize a (method, path) pair into a comparison key with route parameters collapsed to {} (routeMatching.ts). Matching couples only to method and path structure, so it survives server-side route parameter renames.

Co-Authored-By: Dennis van der Stelt <dvdstelt@gmail.com>
Co-Authored-By: williambza <william.brander@gmail.com>
Add AllowedRoutesStore, which fetches the my/routes manifest from the Primary and Monitoring instances, merges them, and dedupes concurrent fetches. Add the useAllowedRoutes composable (canCall / canAnyCall) that resource-owning view models build on, plus the startup fetch in App.vue. Gating is fail-open until the user is authenticated and the manifest has loaded, but a malformed manifest entry is skipped rather than aborting the load, so a single bad entry can never leave the store unloaded and silently fail every gate open.

Co-Authored-By: Dennis van der Stelt <dvdstelt@gmail.com>
Co-Authored-By: williambza <william.brander@gmail.com>
Hide navigation items (PageHeader.vue) and Configuration sub-tabs (ConfigurationView.vue) whose backing ServiceControl route the current user is not allowed to call.

Co-Authored-By: Ramon Smits <ramon.smits@gmail.com>
Co-Authored-By: Dennis van der Stelt <dvdstelt@gmail.com>
Add PermissionGate, a wrapper that disables a slotted action and shows a tooltip with the reason when the user lacks the backing route, instead of hiding the control. Apply it to failed-message, deleted-message and message-group actions, custom-check dismiss, heartbeat endpoint instances, redirects, health-check notifications, edit and retry, and monitored endpoint deletion. Resource-owning stores expose the capability getters these controls bind to. The edit/config fetch is skipped for users without edit permission and treats a 403 as unavailable rather than logging an error.

Co-Authored-By: Ramon Smits <ramon.smits@gmail.com>
Co-Authored-By: williambza <william.brander@gmail.com>
Render a Configuration page that lists the capabilities derived from the current user's allowed routes, with the supporting route and router-link entries.

Co-Authored-By: Ramon Smits <ramon.smits@gmail.com>
Co-Authored-By: williambza <william.brander@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants