ab57e53b92
- Driver.AbCip.Contracts-001: parse 'writable' from TagConfig JSON (default true) instead of hardcoding - Driver.AbCip.Contracts-002/-003: Dt type comment; drop dead [Display]/[Range] annotations - Driver.AbCip.Contracts-004: dedicated AbCipEquipmentTagParser test class (+15) - Driver.AbCip-017: document Tick severity Low-fallback on Bad severity read - Driver.AbLegacy.Contracts-002/-003/-004: isArray-scalar remarks (+tests), MaxTagBytes/ForFamily docs - Driver.Galaxy.Browser-003 + Driver.Galaxy.Contracts-003: extract ResolveApiKey -> GalaxySecretRef (dedup) - Driver.Galaxy-019: cache buffered-interval only on Ok + ILogger warnings + ClassifyIntervalReply (+tests) - Driver.FOCAS.Contracts-002: thread WriteIdempotent through DiscoverAsync (+test)
172 lines
7.7 KiB
Markdown
172 lines
7.7 KiB
Markdown
# Code Review — Driver.Galaxy.Browser
|
||
|
||
<!-- Template for a per-module findings file. Copy to code-reviews/<Module>/findings.md.
|
||
See ../../REVIEW-PROCESS.md for the full process. The base README.md is generated
|
||
from these files by regen-readme.py — do not edit README.md by hand. -->
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Module | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser` |
|
||
| Reviewer | Claude Code |
|
||
| Review date | 2026-06-19 |
|
||
| Commit reviewed | `7286d320` |
|
||
| Status | Reviewed |
|
||
| Open findings | 0 |
|
||
|
||
## Checklist coverage
|
||
|
||
A comprehensive review completes every category, recording "No issues found" where
|
||
a category produced nothing rather than leaving it blank.
|
||
|
||
| # | Category | Result |
|
||
|---|---|---|
|
||
| 1 | Correctness & logic bugs | Driver.Galaxy.Browser-001 (High): MapSecurityClass codes are shifted — mismatches the runtime SecurityMap |
|
||
| 2 | OtOpcUa conventions | No issues found |
|
||
| 3 | Concurrency & thread safety | Driver.Galaxy.Browser-002 (Medium): DisposeAsync has a TOCTOU race on _rootGate.Dispose() |
|
||
| 4 | Error handling & resilience | No issues found |
|
||
| 5 | Security | No issues found |
|
||
| 6 | Performance & resource management | No issues found |
|
||
| 7 | Design-document adherence | No issues found |
|
||
| 8 | Code organization & conventions | Driver.Galaxy.Browser-003 (Low, Resolved): ResolveApiKey duplicated from GalaxyDriver; extracted to `GalaxySecretRef` in Driver.Galaxy.Contracts |
|
||
| 9 | Testing coverage | Driver.Galaxy.Browser-004 (Low): MapSecurityClass not unit-tested; pure static method with no gateway dependency |
|
||
| 10 | Documentation & comments | No issues found |
|
||
|
||
## Findings
|
||
|
||
<!-- One ### entry per finding. IDs are <Module>-NNN, sequential within the module,
|
||
never reused. Findings are never deleted — close them by changing Status and
|
||
completing Resolution. -->
|
||
|
||
### Driver.Galaxy.Browser-001
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Severity | High |
|
||
| Category | Correctness & logic bugs |
|
||
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs:152` |
|
||
| Status | Resolved |
|
||
|
||
**Description:** `GalaxyBrowseSession.MapSecurityClass` maps Galaxy `security_classification`
|
||
integer codes incorrectly. The Browser's switch arms are:
|
||
|
||
| Code | Browser output | Runtime `SecurityMap` / `SecurityClassification` enum |
|
||
|---|---|---|
|
||
| 0 | "FreeAccess" | FreeAccess ✓ |
|
||
| 1 | "Operate" | Operate ✓ |
|
||
| 2 | "Tune" | **SecuredWrite** ✗ |
|
||
| 3 | "Configure" | **VerifiedWrite** ✗ |
|
||
| 4 | "ViewOnly" | **Tune** ✗ |
|
||
| 5 | "Unknown(5)" | **Configure** ✗ |
|
||
| 6 | "Unknown(6)" | **ViewOnly** ✗ |
|
||
|
||
Codes 2–6 are all wrong. The AdminUI attribute side-panel labels "SecuredWrite" attributes
|
||
as "Tune" and "VerifiedWrite" attributes as "Configure", causing operators to misread
|
||
write-protection levels when selecting Galaxy tags. A tag the operator thinks is
|
||
"Tune"-restricted is actually read-only (SecuredWrite / VerifiedWrite → ViewOnly from
|
||
the OPC UA server's perspective). The correct mapping is already implemented in the
|
||
sibling `Driver.Galaxy/Browse/SecurityMap.cs` and matches the `SecurityClassification`
|
||
enum's integer assignments exactly.
|
||
|
||
**Recommendation:** Fix `MapSecurityClass` to match `SecurityClassification` enum ordinals:
|
||
0→FreeAccess, 1→Operate, 2→SecuredWrite, 3→VerifiedWrite, 4→Tune, 5→Configure,
|
||
6→ViewOnly; unknown → `"Unknown({raw})"`. Add a unit test asserting each code.
|
||
|
||
**Resolution:** Fixed 2026-06-19. Corrected all seven code-to-label arms in
|
||
`MapSecurityClass` to match `SecurityClassification` enum ordinals. Regression tests
|
||
`MapSecurityClass_maps_all_known_codes` and
|
||
`MapSecurityClass_unknown_code_returns_Unknown_label` added.
|
||
|
||
---
|
||
|
||
### Driver.Galaxy.Browser-002
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Severity | Medium |
|
||
| Category | Concurrency & thread safety |
|
||
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs:167` |
|
||
| Status | Resolved |
|
||
|
||
**Description:** `DisposeAsync` uses a non-atomic read-then-write pattern on the
|
||
`_disposed` volatile field:
|
||
|
||
```csharp
|
||
if (_disposed) return;
|
||
_disposed = true;
|
||
_rootGate.Dispose();
|
||
```
|
||
|
||
Two concurrent callers can both observe `_disposed == false`, both set it to `true`, and
|
||
both call `_rootGate.Dispose()`. The second call throws `ObjectDisposedException` from
|
||
`SemaphoreSlim.Dispose()`, which propagates uncaught because the `try/catch` that
|
||
follows only wraps `_client.DisposeAsync()`. The `BrowseSessionReaper` (background
|
||
service) and a browser-side "Close" button can race in production.
|
||
|
||
**Recommendation:** Wrap `_rootGate.Dispose()` in a `try/catch (ObjectDisposedException)`
|
||
mirroring the existing client disposal pattern, or use an atomic int guard via
|
||
`Interlocked.Exchange`.
|
||
|
||
**Resolution:** Fixed 2026-06-19. Added `try { _rootGate.Dispose(); } catch (ObjectDisposedException) { }`
|
||
mirroring the existing pattern for `_client.DisposeAsync()`. Regression test
|
||
`DisposeAsync_concurrent_calls_do_not_throw` added.
|
||
|
||
---
|
||
|
||
### Driver.Galaxy.Browser-003
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Severity | Low |
|
||
| Category | Code organization & conventions |
|
||
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyDriverBrowser.cs:149` |
|
||
| Status | Resolved |
|
||
|
||
**Description:** `GalaxyDriverBrowser.ResolveApiKey` is a verbatim copy of
|
||
`GalaxyDriver.ResolveApiKey`. The comment acknowledges this and explains why the Browser
|
||
project intentionally does not reference Driver.Galaxy. However, Finding
|
||
Driver.Galaxy.Browser-001 (the `MapSecurityClass` drift) demonstrates that duplicated
|
||
logic diverges. If a new secret-ref prefix (e.g. `vault:`) is added to the runtime
|
||
resolver, the Browser version will silently fall through to the cleartext-literal arm
|
||
and emit a spurious warning.
|
||
|
||
**Recommendation:** Extract `ResolveApiKey` into the `Driver.Galaxy.Contracts` project,
|
||
which both `Driver.Galaxy` and `Driver.Galaxy.Browser` already reference. This is a
|
||
one-line addition to Contracts — no migration, no public-contract break.
|
||
|
||
**Resolution:** Resolved 2026-06-20 — closed by the same fix as the cross-referenced
|
||
Driver.Galaxy.Contracts-003. The resolver was extracted into a new
|
||
`public static class GalaxySecretRef` in `Driver.Galaxy.Contracts`
|
||
(`ResolveApiKey(string secretRef, ILogger? logger = null)`), which both Driver.Galaxy
|
||
and Driver.Galaxy.Browser already reference. `GalaxyDriverBrowser.BuildClientOptions`
|
||
now calls `GalaxySecretRef.ResolveApiKey(gw.ApiKeySecretRef, _logger)` and its private
|
||
`ResolveApiKey` copy is deleted — eliminating the drift risk (a future `vault:` prefix
|
||
is now added in one place). Behaviour is preserved exactly: the Browser's `_logger` is
|
||
never null (defaults to `NullLogger`), so the literal-arm cleartext warning still fires.
|
||
The Browser project builds clean against the shared resolver. See
|
||
Driver.Galaxy.Contracts-003 for the full extraction details.
|
||
|
||
---
|
||
|
||
### Driver.Galaxy.Browser-004
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Severity | Low |
|
||
| Category | Testing coverage |
|
||
| Location | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser.Tests/GalaxyBrowseSessionTests.cs` |
|
||
| Status | Resolved |
|
||
|
||
**Description:** `GalaxyBrowseSession.MapSecurityClass` (all seven codes plus the
|
||
`Unknown(N)` fallback) had zero unit-test coverage. The existing test comment correctly
|
||
notes that RootAsync/ExpandAsync/AttributesAsync traversal is blocked by the internal
|
||
transport seam, but `MapSecurityClass` is a pure static method with no gateway dependency
|
||
— it is entirely testable in the unit suite. Finding Driver.Galaxy.Browser-001 (wrong
|
||
mapping for codes 2–6) would have been caught immediately had these tests existed.
|
||
|
||
**Recommendation:** Add `[Fact]` tests for each of the seven known codes (0–6) and the
|
||
unknown-code fallback.
|
||
|
||
**Resolution:** Fixed 2026-06-19. Tests `MapSecurityClass_maps_all_known_codes` and
|
||
`MapSecurityClass_unknown_code_returns_Unknown_label` added, covering all codes 0–6
|
||
plus an out-of-range value.
|