Files
lmxopcua/code-reviews/AdminUI/findings.md
T
Joseph Doherty 3c908f1df0 review(AdminUI): fix null-TagConfig crash, CTS leak, unencoded historian tag
Review at HEAD 7286d320. AdminUI-002: IsValidJson null/blank -> friendly error (was
ArgumentNullException). AdminUI-003: DriverStatusPanel Reconnect/Restart dispose CTS (build-
verified, live /run deferred). AdminUI-005: HistorianWonderware picker URL-encodes tag name.
AdminUI-008: Format round-trip test. 001 (script-page authz) + 004 (hub [Authorize]) left
Open as cross-cutting w/ Host/Security.
2026-06-19 10:52:23 -04:00

11 KiB

Code Review — AdminUI

Field Value
Module src/Server/ZB.MOM.WW.OtOpcUa.AdminUI
Reviewer Claude Code
Review date 2026-06-19
Commit reviewed 7286d320
Status Reviewed
Open findings 2

Checklist coverage

# Category Result
1 Correctness & logic bugs AdminUI-002, AdminUI-005, AdminUI-008 (resolved)
2 OtOpcUa conventions No issues found
3 Concurrency & thread safety AdminUI-003 (resolved)
4 Error handling & resilience AdminUI-006 (won't fix)
5 Security AdminUI-001 (open), AdminUI-004 (open)
6 Performance & resource management No issues found
7 Design-document adherence AdminUI-001 (open)
8 Code organization & conventions AdminUI-007 (won't fix)
9 Testing coverage Addressed via the AdminUI-002/005/008 regression tests
10 Documentation & comments No issues found

Findings

AdminUI-001

Field Value
Severity Medium
Category Security
Location Components/Pages/ScriptEdit.razor:5, Components/Pages/Scripts.razor
Status Open

Description: The Script CRUD surface (/scripts/new, /scripts/{id}, and the /scripts list) is gated by only [Authorize] (any authenticated user), whereas the same editor's Roslyn-analysis backend (ScriptAnalysisEndpoints) requires the FleetAdmin policy, and /deployments requires Administrator,Designer. A virtual-tag script is executable C# that runs server-side in the sandbox at deploy time, so saving/editing script source is a privileged operation; gating it below the analysis backend and the deploy action is an inconsistency in the privilege model. A ReadOnly-tier operator who can authenticate can currently create and edit script bodies (they cannot deploy them, and the runtime is sandboxed, which is why this is Medium and not High).

Recommendation: Align the Script pages' authorization with the privilege model — gate ScriptEdit and Scripts with Authorize(Roles = "Administrator,Designer") (matching /deployments) or the FleetAdmin policy (matching the analysis backend). This is a policy/attribute change on Razor pages; confirm the intended tier with the security owner, since it changes who can reach the editor. Left Open pending that decision (an authorization-policy decision, not a self-contained bug fix).

Resolution: (open)

AdminUI-002

Field Value
Severity Low
Category Correctness & logic bugs
Location Uns/UnsTreeService.cs:1284 (IsValidJson)
Status Resolved

Description: CreateTagAsync/UpdateTagAsync validate the tag config with IsValidJson(input.TagConfig), which calls JsonDocument.Parse(json). The catch only handles JsonException. If input.TagConfig is null, JsonDocument.Parse((string)null) throws ArgumentNullException, which escapes IsValidJson and faults the mutation with an unhandled exception (unfriendly 500) instead of returning the intended "TagConfig is not valid JSON." result. The TagModal always supplies "{}", so this is latent, but the service is a public seam (also exercised by tests) and should treat null defensively.

Recommendation: Treat null/blank as invalid in IsValidJson (return false on null/whitespace) so the friendly result is returned for every caller.

Resolution: Resolved 2026-06-19 — IsValidJson returns false for null/whitespace before JsonDocument.Parse, so a null TagConfig yields the friendly "not valid JSON" result instead of an unhandled ArgumentNullException. TDD: UnsTreeServiceTagTests.CreateTag_with_null_TagConfig_returns_friendly_error and ...with_blank_TagConfig_returns_friendly_error (failing → fixed → passing). (SHA pending.)

AdminUI-003

Field Value
Severity Low
Category Concurrency & thread safety
Location Components/Shared/Drivers/DriverStatusPanel.razor:231, :256
Status Resolved

Description: ReconnectAsync and RestartConfirmedAsync create a CancellationTokenSource inline (new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(15)).Token) and never dispose it. A CancellationTokenSource with a timeout schedules a Timer; not disposing it leaks that timer until finalization. The sibling Alerts.razor does this correctly with using var cts = .... Each Reconnect/Restart click leaks one CTS + timer for the circuit's lifetime.

Recommendation: Use using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15)); and pass cts.Token, matching Alerts.razor.

Resolution: Resolved 2026-06-19 — both handlers now use using var cts and pass cts.Token, so the CTS (and its timer) is disposed when the handler returns. Razor-behavioural change verified by build only — live /run deferred (no bUnit). (SHA pending.)

AdminUI-004

Field Value
Severity Low
Category Security
Location Hubs/FleetStatusHub.cs, Hubs/AlertHub.cs, Hubs/ScriptLogHub.cs (vs Hubs/DriverStatusHub.cs:12)
Status Open

Description: Of the four SignalR hubs mapped by MapOtOpcUaHubs, only DriverStatusHub carries an explicit [Authorize] attribute. FleetStatusHub, AlertHub, and ScriptLogHub (which broadcast fleet status, alarm transitions with equipment paths/messages, and script-log diagnostic lines) have no explicit authorization metadata. In the current Host wiring they ARE protected against unauthenticated access by the global FallbackPolicy = RequireAuthenticatedUser (Security/ServiceCollectionExtensions.cs:144), so this is not an open hole today — but the protection is implicit and inconsistent. If the fallback policy were ever relaxed, or a hub mapped from a different host without it, these three hubs would silently become anonymous.

Recommendation: Add [Authorize] to FleetStatusHub, AlertHub, and ScriptLogHub to match DriverStatusHub, so each hub's authorization requirement is explicit and does not rely solely on the host's fallback policy. Low-risk (the fallback already requires auth, so observable behaviour is unchanged) but left Open because hub authorization is a cross-cutting concern worth confirming with the Host/Security owners rather than silently changing in a single-module pass.

Resolution: (open)

AdminUI-005

Field Value
Severity Low
Category Correctness & logic bugs
Location Components/Shared/Drivers/Pickers/HistorianWonderwareAddressBuilder.cs:11
Status Resolved

Description: HistorianWonderwareAddressBuilder.Build interpolates the tag name straight into a query string: $"{tagName}?mode={mode}&interval={interval}". A historian tag name containing ?, &, #, or = would corrupt the produced address (the part after a & in the name would be parsed as a spurious query parameter by any downstream consumer that splits on &). Historian tag names are typically dotted identifiers, so this is unlikely in practice, but the builder is unit-tested as a pure helper and should produce a well-formed string for any input.

Recommendation: URL-encode the tag name (Uri.EscapeDataString(tagName)) before composing the query string.

Resolution: Resolved 2026-06-19 — the tag name is Uri.EscapeDataString-encoded before being placed in the query string, so a name containing reserved characters produces a well-formed address. TDD: HistorianWonderwareAddressBuilderTests.Build_escapes_reserved_characters_in_tag_name (failing → fixed → passing). (SHA pending.)

AdminUI-006

Field Value
Severity Low
Category Error handling & resilience
Location Uns/UnsTreeService.cs:368, :494, :707, :993, :1191, :1518
Status Won't Fix

Description: The delete paths catch a bare Exception and surface ex.Message verbatim into the operator-facing UnsMutationResult.Error (e.g. $"Delete failed: {ex.Message}. Likely because …"). On a real SQL Server an FK-violation message can include server/schema/constraint detail. This is low risk (the AdminUI is an authenticated fleet-admin surface, not public, and the messages are deliberately operator-actionable) but echoing raw provider exception text into the UI is a minor information-exposure / robustness smell.

Recommendation: Either keep as-is (intentional operator guidance) or log ex at Warning and surface a fixed message without the raw ex.Message.

Resolution: Won't Fix (2026-06-19) — intentional design. The AdminUI is an authenticated fleet-admin-only surface and the raw message is paired with deliberate operator guidance ("…remove its children first"). Removing the provider text would reduce diagnosability for the operators who own these operations. Recorded as a known, accepted trade-off.

AdminUI-007

Field Value
Severity Low
Category Code organization & conventions
Location Clients/AdminOperationsClient.cs:37, :52, :69
Status Won't Fix

Description: StartDeploymentAsync, AcknowledgeAlarmAsync, and ShelveAlarmAsync pass BOTH an explicit AskTimeout and a linked CTS that itself already has CancelAfter(AskTimeout): _proxy.Ask<T>(msg, AskTimeout, linked.Token). The two timeouts are redundant (the Ask-level timeout and the token both fire at 10s). Harmless, but the doubled timeout is slightly confusing — the AskAsync<T> generic at :78 correctly relies on the caller's token alone.

Recommendation: Drop one of the two — either pass only linked.Token (relying on the CTS timeout) or only AskTimeout (dropping the linked CTS). Cosmetic.

Resolution: Won't Fix (2026-06-19) — the belt-and-suspenders pairing is harmless and the two mechanisms guard slightly different failure modes (the Ask timeout is Akka's own deadline; the linked CTS also honours caller cancellation). Not worth a behavioural change to a working control-plane client in a review pass.

AdminUI-008

Field Value
Severity Low
Category Testing coverage
Location ScriptAnalysis/ScriptAnalysisService.cs:376 (Format)
Status Resolved

Description: Format documents a "return the original source unchanged on un-formattable input" contract (the catch returns req.Code), but no regression test locked that contract for null/blank or unparseable input, so a future change to the catch could silently break the editor's format action. (No behavioural bug found; this is a test-coverage gap on a documented contract.)

Recommendation: Add a regression test asserting Format returns the input unchanged for null/blank/garbage input and round-trips valid input.

Resolution: Resolved 2026-06-19 — added FormatTests.Format_returns_input_unchanged_for_null_or_empty and Format_returns_input_for_unparseable_garbage, locking the return-original contract (the existing FormatTests already covered the happy path). Coverage-only; no production change. (SHA pending.)