Gate ServicePulse UI on user permissions#3041
Open
dvdstelt wants to merge 9 commits into
Open
Conversation
Gate UI on the current user's permissions, fetched from ServiceControl's GET api/my/permissions/all (a flat list of permission strings such as "error:messages:retry"; see ServiceControl PR #5538). - PermissionsStore: holds the user and a Set<string> of granted permissions. - usePermissions: can(permission)/canAny(permissions) (set membership) plus fetchDescriptor() (deduped, fail-safe). Gating is fail-open — it only applies once authorization is enabled, the user is authenticated and the descriptor has loaded, so the UI is unchanged for auth-disabled/older deployments. No UI consumes this yet.
Not wired into the UI. ServiceControl does not yet support per-resource (per-queue) scopes — limiting a permission to queues like "sales.*" — so this is kept ready for when it does, at which point can(permission, resource) can enforce a grant's allow/deny patterns. - permissionMatching: pure matchesPattern/entryPermits (exact, prefix.*, *, deny-wins, case-insensitive), self-contained. - scope-vectors.json: shared test vectors so the frontend matcher and the future ServiceControl ResourceScope/FilterByQueueScope cannot drift; both suites run the same cases.
refresh() runs from a watch/auto-refresh, so a rejected throughput connection test (ServiceControl unreachable, or the auth token cleared during logout) surfaced as an unhandled promise rejection. Catch and log instead.
PageHeader now shows each nav item only when the user holds the matching ServiceControl permission (e.g. Heartbeats -> error:heartbeats:view, Throughput -> error:throughput:view, Monitoring -> monitoring:endpoint:view, Audit -> audit:message:view), replacing the coarse 7-bool usePermissionGate. - App.vue loads my/permissions/all once authenticated. - usePermissions exposes ready (gate-on-ready): the nav renders nothing until permissions are known, so items never appear and then disappear. Fail-open on auth-disabled / failed load (loadAttempted settles, can() permits). - Move the descriptor fetch + in-flight dedupe into PermissionsStore so each Pinia instance owns it; a module-level singleton leaked a stale request across app re-mounts, leaving ready stuck false. - Tests: hasUserPermissions precondition (mocks my/permissions/all, granted by default when auth is enabled); ready unit tests. ConfigurationView still uses the old summary path until the config-tab step (F5).
Each failed-message action button now shows only when the user holds the matching ServiceControl permission, in addition to its existing visibility conditions: - Retry -> error:messages:retry - Edit & retry -> error:messages:edit - Delete -> error:messages:archive - Restore -> error:messages:unarchive Fail-open via can() (unchanged for auth-disabled deployments). Verb-level only; the per-queue resource argument is deferred until ServiceControl supports scopes. Adds messageButtonPermissions.spec.ts covering shown-when-granted/hidden-when-denied.
ConfigurationView tabs now gate on the specific ServiceControl permission each needs, via usePermissions (gate-on-ready, consistent with the nav): - License -> error:licensing:view - Usage Setup -> error:throughput:manage - MassTransit -> error:connections:view - Health Checks -> error:notifications:view - Retry Redirects-> error:redirects:view - Connections -> error:connections:view - Endpoint Conn. -> error:endpoints:view - User Permissions tab -> shown when auth is enabled Connections / Endpoint Connection were previously ungated; they now require the permission the server actually enforces. The User Permissions page is reworked to render the new flat descriptor (user + sorted list of granted permission strings) instead of the retired 7-bool matrix.
Nothing reads the old summary path anymore (PageHeader, ConfigurationView and the User Permissions page now use usePermissions / PermissionsStore), so delete it: - usePermissionGate composable (+ spec) - UserPermissionsStore (+ spec) - App.vue: drop the old summary-fetch watch and its supportsUserPermissions gate, leaving only the my/permissions/all load - EnvironmentAndVersionsStore: drop the now-unused supportsUserPermissions, mypermissions_all_url and mypermissions_summary_url fields
Replace the flat list of permission strings with a table per instance (Error/ Audit/Monitoring): features as rows, actions as columns, a checkmark where the action is granted. All tables share the same column set, ordered most-widely- shared first (View, Delete, then the rest) so columns line up across instances; a column shows its header and checks only for instances that use it. Feature names are left-aligned and action columns have a fixed width, so the table shrinks to fit and stays left-aligned (a view-only user sees just Feature | View). Features and instances with no granted permission are omitted.
Across the failed-message screens, retry/delete/restore actions stay visible but are disabled with a tooltip explaining why when the user lacks the permission, instead of silently failing server-side: - Groups list: Request retry / Delete group -> error:recoverabilitygroups:retry / :archive - All failed messages toolbar: Retry/Delete selected -> error:messages:retry / :archive; Retry all / Delete all -> error:recoverabilitygroups:retry / :archive - Deleted message groups: Restore group -> error:recoverabilitygroups:unarchive - Deleted messages toolbar: Restore selected -> error:messages:unarchive A shared PermissionGate wrapper centralises the behaviour: it shows the tooltip (via a span so it appears even over a disabled button), lets pointer events reach the wrapper, and tidies the disabled look of link-style buttons. Toolbar spacing for the wrapper is handled by the existing .btn-toolbar rule in main.css. Adds PermissionGate (+ spec) and messageGroupButtonPermissions.spec.ts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this PR builds on #3035, and what changes
#3035 (with the follow-up fixes in #3036) established this feature: the idea of hiding UI a user cannot act on, the permission plumbing, the User Permissions page, and a working end-to-end flow. This PR stands on that foundation. What it changes is the underlying model used to decide what to show, and we think that change is worth merging. None of this is a knock on the original approach; it is the natural next iteration now that the per-permission endpoint exists.
The core difference
failed_messages_read/write,auditing_read,monitoring_read/write,admin_read/write)error:messages:retry,error:heartbeats:view, …)In short, #3035 introduces an intermediate, coarser vocabulary and asks the frontend to map every feature back onto it. This PR removes that middle step so the UI mirrors the server's own permission decision directly.
What this buys us
It removes a class of mapping mistakes. With two independent mappings and no shared source of truth, the two can drift. That already happened once and was caught in review: Throughput was initially gated on
failed_messages_readand Review fixes for user authorization tab hiding (#3035) #3036 corrected it toadmin_read. It is simply an easy thing to get subtly wrong. In this PR there is no bucket to assign, Throughput gates onerror:throughput:view(the exact permission the server checks), so the mapping is mechanical.It is fine-grained where the summary is coarse. Hide tabs based on user authorization levels #3035's single
failed_messages_writecovers retry, edit, archive, unarchive and delete together. This PR gates each action on its own permission (error:messages:retry,:edit,:archive,:unarchive), so a user can be granted retry without edit. Configuration tabs likewise gate on the specific permission each one needs instead of a singleadmin_read.It matches the server's real authorization model. We checked against ServiceControl's
Permissions.csandRolePermissions(reader = *:*:view,writer = *:*:*): the strings in this PR checks are exactly the ones the server grants and enforces, rather than a separate set of buckets.Fewer moving parts, while doing more. This PR removes the summary endpoint usage,
usePermissionGate,UserPermissionsStore, thesupportsUserPermissionscapability flag and themypermissions_*_urlplumbing, and still offers finer control.It leaves a clean path to per-resource scopes. The same vocabulary extends to per-queue checks,
can("error:messages:retry", queueName), with no structural change. This PR includes the scope matcher in a dormant state plus a sharedscope-vectors.jsonfixture, so turning on per-queue authorization later is additive. The 7-boolean summary has no way to express per-queue scoping.Clearer runtime behavior. Gate-on-ready avoids items flashing in and then disappearing, the UI fails open when auth is disabled or the descriptor cannot be loaded (so non-OIDC deployments are unaffected), and there is broader test coverage (matcher, composable, store and button gating).
Additionally
Disabled-with-tooltip actions on the appropriate screens
Where Hide tabs based on user authorization levels #3035 only hid tabs, the retry/delete/restore actions now
stay visible but become disabled, with a tooltip explaining why, when you lack
the permission. This keeps the capability discoverable and stops clicks from
silently failing server-side.
A shared
PermissionGatecomponent centralizes this (tooltip on a wrapper soit shows even over a disabled button, plus the disabled styling).
Redesigned User Permissions page. Instead of the flat boolean matrix, it
now groups your permissions by instance (Error / Audit / Monitoring) and by
feature (Messages, Heartbeats, Custom checks, ...), with a checkmark per
granted action in an aligned, compact, left-aligned matrix. Features and
instances you have no access to are omitted, and hovering a feature shows the
exact permission strings you hold for it.
A small robustness fix surfaced along the way: the throughput auto-refresh
now handles errors instead of throwing an unhandled promise rejection (e.g.
when the token is cleared on logout).
Dormant groundwork for per-queue scopes (the scope matcher plus a shared
scope-vectors.jsontest fixture), so per-resource authorization can be addedlater without restructuring.
Trade-offs
my/permissions/allendpoint (#5538). Against an older ServiceControl it returns 404 and the UI fails open.admin_read. This is closer to what the server actually allows (a reader genuinely has:view), but it is worth being aware of.Bottom line
#3035 proved the feature and got it working end to end. This PR keeps that and changes the model underneath so the frontend asks the server the same authorization question the server already answers. That removes the hand-maintained translation, makes per-action control possible, keeps the UI aligned with what the server enforces, and leaves an additive path to per-queue scoping. Happy to walk through any part of it together and adjust based on feedback.