fix(central-ui): Flow Debugger Replay opens to OperationalAudit roles; Live run and Retry/Discard stay Deployer-only (arch-review F3)
Design D6 / §9 says a Live run is a Deployer act while Replay needs only
OperationalAudit, but the page and all three FlowDebug* commands were gated on
Deployer. The Audit Log and Execution Chain pages -- themselves OperationalAudit
-- show an "Open in Flow Debugger" drill-in that an auditor could not follow.
Implements the split:
- New `RequireFlowDebugger` policy over `FlowDebuggerAccessRoles`
{Deployer, Administrator, Viewer}; the page now carries it instead of
`RequireDeployment`.
- `FlowDebugReplayCommand` takes the same role set in the ManagementActor
matrix; `FlowDebugRunMethodCommand` / `FlowDebugRunValueChangeCommand` stay
DeployerOnly.
- The page resolves `_canLive` (Deployer) in OnInitializedAsync and, without it,
pins the mode to Replay, hides the entry-point and mode toggles, the Run
button and the Live pickers, honours only the `?executionId=` drill-in, and
leaves the parked-node Retry/Discard callbacks unset so those buttons never
render. `CanRun`, `SetMode(Live)` and `ActOnStepAsync` repeat the check so the
split does not rest on markup alone.
The NavMenu entry stays inside the Deployer-gated Deployment section -- auditors
arrive through the audit drill-ins, which is the point of the finding.
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
+40
-18
@@ -3,7 +3,10 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Deployment
|
||||
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
|
||||
@* arch-review F3: opening the page is a Replay-capable read (Deployer + the OperationalAudit
|
||||
roles) so the Audit Log / Execution Chain drill-ins work for an auditor. Every Live-run
|
||||
affordance below is additionally gated on _canLive (the Deployer role). *@
|
||||
@attribute [Authorize(Policy = AuthorizationPolicies.RequireFlowDebugger)]
|
||||
|
||||
<PageTitle>Flow Debugger</PageTitle>
|
||||
|
||||
@@ -13,15 +16,19 @@
|
||||
|
||||
@* ── Toolbar ─────────────────────────────────────────────────────────── *@
|
||||
<div class="row g-2 align-items-end mb-2" data-test="flow-toolbar">
|
||||
<div class="col-auto">
|
||||
<label class="form-label form-label-sm">Entry point</label>
|
||||
<div class="btn-group btn-group-sm d-block" role="group" aria-label="Entry point">
|
||||
<button type="button" class="btn @(_entry == EntryKind.Method ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-entry-method" disabled="@_running" @onclick="() => SetEntryAsync(EntryKind.Method)">Inbound API method</button>
|
||||
<button type="button" class="btn @(_entry == EntryKind.ValueChange ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-entry-value-change" disabled="@_running" @onclick="() => SetEntryAsync(EntryKind.ValueChange)">Value-change trigger</button>
|
||||
@* The entry point only selects what a Live run starts from, so it is Deployer-only. *@
|
||||
@if (_canLive)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<label class="form-label form-label-sm">Entry point</label>
|
||||
<div class="btn-group btn-group-sm d-block" role="group" aria-label="Entry point">
|
||||
<button type="button" class="btn @(_entry == EntryKind.Method ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-entry-method" disabled="@_running" @onclick="() => SetEntryAsync(EntryKind.Method)">Inbound API method</button>
|
||||
<button type="button" class="btn @(_entry == EntryKind.ValueChange ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-entry-value-change" disabled="@_running" @onclick="() => SetEntryAsync(EntryKind.ValueChange)">Value-change trigger</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_mode == FlowMode.Live)
|
||||
{
|
||||
@@ -85,15 +92,19 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-auto">
|
||||
<label class="form-label form-label-sm">Mode</label>
|
||||
<div class="btn-group btn-group-sm d-block" role="group" aria-label="Mode">
|
||||
<button type="button" class="btn @(_mode == FlowMode.Live ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-mode-live" disabled="@_running" @onclick="() => SetMode(FlowMode.Live)">Live</button>
|
||||
<button type="button" class="btn @(_mode == FlowMode.Replay ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-mode-replay" disabled="@_running" @onclick="() => SetMode(FlowMode.Replay)">Replay</button>
|
||||
@* Without Deployer there is only Replay, so the toggle would be a single dead button. *@
|
||||
@if (_canLive)
|
||||
{
|
||||
<div class="col-auto">
|
||||
<label class="form-label form-label-sm">Mode</label>
|
||||
<div class="btn-group btn-group-sm d-block" role="group" aria-label="Mode">
|
||||
<button type="button" class="btn @(_mode == FlowMode.Live ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-mode-live" disabled="@_running" @onclick="() => SetMode(FlowMode.Live)">Live</button>
|
||||
<button type="button" class="btn @(_mode == FlowMode.Replay ? "btn-primary" : "btn-outline-secondary")"
|
||||
data-test="flow-mode-replay" disabled="@_running" @onclick="() => SetMode(FlowMode.Replay)">Replay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-auto ms-auto">
|
||||
@if (_mode == FlowMode.Live)
|
||||
@@ -115,6 +126,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_canLive)
|
||||
{
|
||||
<div class="text-muted small mb-2" data-test="flow-replay-only">
|
||||
Replay only — rebuilding a past execution from the audit log. Starting a live run
|
||||
requires the Deployer role.
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Inline alerts ───────────────────────────────────────────────────── *@
|
||||
@if (_error != null)
|
||||
{
|
||||
@@ -232,10 +251,13 @@
|
||||
|
||||
@* ── Detail panel ──────────────────────────────────────────────────── *@
|
||||
<div class="col-12">
|
||||
@* Retry / Discard relay a command to the site — only wired for a Deployer, so the
|
||||
buttons never render for an auditor (arch-review F3). *@
|
||||
<FlowStepDetail Step="_selected" Graph="_graph"
|
||||
SiteDbId="@(_entry == EntryKind.ValueChange && _selectedSiteId > 0 ? _selectedSiteId : null)"
|
||||
InstanceDbId="@(_entry == EntryKind.ValueChange && _selectedInstanceId > 0 ? _selectedInstanceId : null)"
|
||||
OnRetry="OnRetryStepAsync" OnDiscard="OnDiscardStepAsync" ActionInProgress="_actionInProgress" />
|
||||
OnRetry="RetryCallback" OnDiscard="DiscardCallback"
|
||||
ActionInProgress="_actionInProgress" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user