Skip to content

feat(dashboard): open the dashboard to every user and gate widgets by permission - #1103

Merged
sfmskywalker merged 5 commits into
release/3.9.0from
feat/1099-dashboard-widget-permissions
Oct 1, 2026
Merged

sfmskywalker merged 5 commits into
release/3.9.0from
feat/1099-dashboard-widget-permissions

Conversation

@sfmskywalker

Copy link
Copy Markdown
Member

Closes #1099

The Dashboard, the landing page, was all-or-nothing: dashboard:view or an access-denied page. Now every signed-in user can open it, and each widget shows only to users permitted to see its data. The design decisions are recorded on #1099: hidden rather than disabled, gated by the data's own permission, and a welcome panel when nothing is visible.

Requires elsa-workflows/elsa-core#8562, which gates the dashboard API per section. Merge that first or together with this. Against an older core, restricted users with a visible widget would get "No access" from the overview.

Change

  • Access: the Dashboard page and its menu item no longer require dashboard:view.

  • Widget permissions:

    • DashboardWidgetDescriptor gains RequiredPermissions (init-only). A widget is visible when the user holds any of them; none declared means everyone; unknown permissions fail open, as everywhere in Studio.
    • AddDashboardWidget keeps its signature and gains an overload that takes the permissions, so already-compiled callers keep working. UserPermissions.HasAny is new.
  • Built-in widgets declare dashboard:view plus the permission of their data:

    • workflows/instances:view for the instance metrics, trends, recent activity and needs-attention widgets;
    • diagnostics/structured-logs:view and diagnostics/console-logs:view for the log summaries.

    The OpenTelemetry widget declares only diagnostics/opentelemetry:view, because its figures come from the OpenTelemetry API, which dashboard:view never opened. The README records this.

  • Runtime status (the header chip) shows with dashboard:view or workflows/runtime:view, including for a user with no widget.

  • Only the data the page needs. The overview loads for any visible widget or the runtime chip. The four workflow-instance endpoints load only when a visible widget declares workflows/instances:view and the user holds it or dashboard:view.

  • Nothing refused is shown as an error.

    • A section the backend marks Unauthorized is left out.
    • A 401/403 from a secondary instance endpoint keeps the overview. For example, the metrics widget drops its "Needs attention" card rather than showing 0.
    • A 403 on the overview itself still shows "No access".
  • Welcome panel. A user with no visible widget sees "No dashboard widgets are available to your role" with shortcuts to the pages they can open, in navigation order (MenuServiceExtensions.GetPages, returning MenuPage). With no pages at all, the existing no-pages notice shows. If the menu fails, the panel shows without shortcuts and logs a warning.

  • Late widgets.

    • Companion widgets register after remote features initialize, which is after the landing page mounts. The new IFeatureService.IsInitialized (a default interface member, forwarded by EnvironmentAwareFeatureService) tells "still registering" apart from "none".
    • The wait is bounded to 3 seconds (TimeProvider, registered by the module), so hosts that never initialize features settle to the welcome panel. Widgets that register later still appear.
  • Page lifetime. The page re-checks disposal inside its queued callback, and disposes its timer and subscription on disposal.

  • PermissionPageGuard's landing redirect (feat(shell): send users who can't view the landing page to their first accessible page #1093) stays as a generic fallback for hosts whose / is another gated page. The Dashboard no longer triggers it.

  • The Dashboard README (permission mapping for widget authors), doc/DASHBOARD_ARCHITECTURE_BLUEPRINT.md and specs/007-operational-dashboard/prd.md describe the access model.

Tests

  • DashboardPagePermissionTests checks, for each case, which widgets render and which endpoints are called:
    • workflow instances only; runtime only (chip shown or withheld); structured logs only; OpenTelemetry only;
    • dashboard:view, including on a host with only diagnostics widgets (overview only);
    • no permissions, unknown permissions, and unknown permissions with a 403 from an instance endpoint;
    • a 403 on the overview; a permission change that widens the scope and reloads, or removes every widget and shows the welcome panel;
    • features that never initialize (bounded wait) and late widget registration;
    • disposal of the timer and subscription, and no load after disposal.
  • DashboardWelcomeTests covers the menu failing. DashboardWidgetRegistrationTests has a theory over every built-in widget's permissions, plus pin tests tying the mirrored workflow permission constants to WorkflowPermissions.
  • Core tests cover UserPermissions.HasAny and IsInitialized in both feature services.
  • The new tests fail without their fixes, except the in-callback disposal re-check. bUnit's dispatcher runs InvokeAsync inline, so that window can't be reproduced, and the guard is covered by review.
  • dotnet test Elsa.Studio.sln -f net10.0: all green (Dashboard 106). The touched multi-targeted projects build for net8.0.

🤖 Generated with Claude Code

sfmskywalker and others added 3 commits October 1, 2026 00:22
… permission

Every signed-in user can open the Dashboard (#1099): the page and its menu
item no longer require dashboard:view. Each widget is shown only to users
permitted to see its data, and hidden from everyone else.

- DashboardWidgetDescriptor gains an init-only RequiredPermissions (visible
  with any of them; none declared means everyone) and AddDashboardWidget a
  trailing requiredPermissions parameter. Built-in widgets declare
  dashboard:view or the view permission of their data (workflows/instances,
  diagnostics/structured-logs, diagnostics/console-logs); the OpenTelemetry
  widget, which loads from its own API, declares diagnostics/opentelemetry:view.
- The page requests only what its visible widgets need, from endpoints the
  user may call (elsa-core#8561): nothing without a widget, the overview for
  any widget, and the workflow instance endpoints only with dashboard:view or
  workflows/instances:view.
- A section the backend withholds (Capability Unauthorized, now also on the
  runtime and workflow instance sections) is left out rather than reported.
  The runtime chip needs dashboard:view or workflows/runtime:view.
- A user with no visible widget gets a welcome panel with shortcuts to the
  pages they can open, in navigation order, or the no-pages notice.
- IFeatureService.IsInitialized lets the page tell widgets that are still
  being registered from no widgets at all, so the welcome panel does not flash
  while remote features initialize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on refused instance endpoints

- Stop waiting for feature initialization after a few seconds so hosts that never initialize them settle on the welcome.
- Load the overview for a user who may only read the runtime status and show it above the welcome shortcuts.
- Keep the overview when an instance endpoint answers 401/403; the widgets needing the refused part show nothing.
- Cache the permitted widgets, degrade the welcome when the menu fails, and have GetPages return normalised hrefs.
- Keep the original AddDashboardWidget signature and add a permissions overload; pin WorkflowRuntime to WorkflowPermissions.Runtime.
- Share one StubFeatureService across the dashboard tests and add the page-level permission matrix.
- Reconcile the PRD, blueprint and README with the final behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…age lifetime

- Re-check disposal inside the queued callback and swallow the exceptions of a torn-down dispatcher.
- Load the instance endpoints only when a permitted widget declares workflows/instances:view.
- Keep the OpenTelemetry widget gated by diagnostics/opentelemetry:view only, and document it.
- Reword the welcome copy, cover its menu-failure branch, hide a withheld backend label.
- Inject TimeProvider (registered by the module); simplify WhenRefusedAsync and RequiredScope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Dashboard access control: opens page to all users, gates widgets by permission.

No outstanding findings prevent merging.

Summary

The dashboard compares permission grants rather than permission objects, so it reloads when grants change without reloading unnecessarily after a token renewal with the same grants.

Reviews (3) · Last reviewed commit: "fix(dashboard): reload only when the gra..."

Comment thread src/modules/Elsa.Studio.Dashboard/Pages/Index.razor.cs
…e same scope

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/modules/Elsa.Studio.Dashboard/Pages/Index.razor.cs Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.

1 participant