Commit Graph

762 Commits

Author SHA1 Message Date
Joseph Doherty e675b34500 feat(sitecallaudit): daily terminal-row purge scheduler
Add a daily purge tick to SiteCallAuditActor that drops terminal SiteCalls
rows older than the retention window via ISiteCallAuditRepository.PurgeTerminalAsync.
The threshold is computed each tick as UtcNow - RetentionDays so an operator who
lowers RetentionDays sees it on the next purge without a restart. Mirrors
AuditLogPurgeActor's daily cadence + continue-on-error posture: a purge fault is
logged and swallowed so the central singleton stays alive and retries next tick.

The purge timer is started in PreStart alongside the reconciliation timer and
gates on the same collaborators (pull client + enumerator) being available — the
repo-only test ctor injects neither, so neither background timer runs there.

Options: PurgeInterval (default 24h, clamped >= 1 min so a zero config value
can't spin the scheduler) + RetentionDays (default 365), plus a test-only
override that bypasses the clamp for millisecond cadences.

Tests (all in-memory, no live MSSQL): purge tick calls PurgeTerminalAsync with a
UtcNow - RetentionDays threshold (non-default 30 days); default retention yields
a 365-day threshold; a throwing repo does not kill the singleton (a second tick
still arrives).
2026-06-15 12:03:49 -04:00
Joseph Doherty e427b38fb3 feat(sitecallaudit): periodic reconciliation pull back-fills lost telemetry
Add a periodic reconciliation tick to SiteCallAuditActor that, per site,
pulls changed SiteCall rows since a per-site UpdatedAtUtc cursor and upserts
them idempotently (monotonic UpsertAsync) — the documented self-heal for lost
best-effort gRPC telemetry. Mirrors SiteAuditReconciliationActor's structure
(per-site cursor, per-site try/catch failure isolation, advance cursor by max
observed UpdatedAtUtc) minus the stalled-detection EventStream machinery.

Dependency wiring: add an acyclic SiteCallAudit -> AuditLog project reference
and resolve IPullSiteCallsClient + ISiteEnumerator (central-only singletons
registered by AddAuditLogCentralReconciliationClient) from the IServiceProvider
the production ctor already holds — no Host Props.Create change needed. The
repo-only test ctor injects neither collaborator, so the tick is gated off
there. A new public test ctor injects fake client + enumerator + repo so the
tick is unit-testable in-memory (public, not internal: Akka's ActivatorProducer
uses public-only reflection binding).

Options: ReconciliationInterval (default 5 min, clamped >= 1s so a zero config
value can't spin the scheduler) + ReconciliationBatchSize (default 500), plus a
test-only override that bypasses the clamp for millisecond cadences.

Tests (all in-memory, no live MSSQL): absent row is upserted on a tick; second
tick advances the cursor past already-pulled rows; one failing site does not
sink other sites; repo-only ctor does not start the tick.
2026-06-15 12:01:22 -04:00
Joseph Doherty 6b0140dd62 fix(sitecallaudit): UpdatedAtUtc index + per-row pull resilience + UTC-convention + first-cycle test (review) 2026-06-15 10:47:25 -04:00
Joseph Doherty 963e3427da feat(sitecallaudit): PullSiteCalls reconciliation plumbing (store read + RPC + site handler + central client)
Site Call Audit (#22): build the documented periodic reconciliation PULL
self-heal path for the eventually-consistent central SiteCalls mirror, as a
dedicated PullSiteCalls gRPC RPC kept separate from the audit pull. This is the
pull PLUMBING only; the central reconciliation tick is a separate follow-up.

- IOperationTrackingStore.ReadChangedSinceAsync(sinceUtc, batchSize): inclusive
  UpdatedAtUtc cursor, oldest-first, batch-capped; SQLite impl projects tracking
  rows onto SiteCallOperational (Kind->Channel, TargetSummary->Target, SourceSite
  left empty - the store has no site-id column).
- sitestream.proto: rpc PullSiteCalls + PullSiteCallsRequest/Response, mirroring
  PullAuditEvents; regenerated checked-in SiteStreamGrpc/*.cs.
- SiteCallDtoMapper.ToDto(SiteCallOperational): inverse of FromDto for the handler.
- SiteStreamGrpcServer.PullSiteCalls handler + SetOperationTrackingStore seam;
  Host wires the seam alongside SetSiteAuditQueue (site roles only).
- Central IPullSiteCallsClient + GrpcPullSiteCallsClient (home: AuditLog/Central to
  reuse ISiteEnumerator; SiteCallAudit does not reference AuditLog). Re-stamps
  SourceSite from the dialed siteId; no-throw on tolerable transport faults;
  SpecifyKind (not ToUniversalTime) cursor handling. Central-only DI registration.

Tests: ReadChangedSinceAsync (4), PullSiteCalls handler (6), GrpcPullSiteCallsClient
(8). Full solution build 0 warnings/0 errors (TreatWarningsAsErrors).
2026-06-15 10:39:06 -04:00
Joseph Doherty c092e89fd1 fix(audit): robust central options binding + interval clamps + doc/contract fixes (review) 2026-06-15 10:11:49 -04:00
Joseph Doherty 36a08a4145 feat(audit): start purge + reconciliation singletons; production ISiteEnumerator 2026-06-15 10:00:44 -04:00
Joseph Doherty d03c2af9a1 fix(audit): race-safe channel cache + UTC-kind cursor handling in gRPC pull client (review) 2026-06-15 09:49:43 -04:00
Joseph Doherty 2adc5767da feat(audit): production gRPC IPullAuditEventsClient for site reconciliation 2026-06-15 09:41:13 -04:00
Joseph Doherty bc8960779b feat(ui): add data-test hooks to InstanceConfigure alarm-override section 2026-06-07 10:10:50 -04:00
Joseph Doherty c84eb5aeef docs(cli): note intentional omission of HiLo per-setpoint priorities/deadbands/messages (review fix) 2026-06-07 10:06:58 -04:00
Joseph Doherty bbc3804d07 feat(cli): typed setpoint flags for template alarm add (serializes trigger-config JSON) 2026-06-07 10:02:51 -04:00
Joseph Doherty 137de66d20 feat(ui): add data-test hooks to DebugView site/instance selects (additive, test-only) 2026-06-06 13:22:32 -04:00
Joseph Doherty c23e2bf227 feat(centralui): add data-test hooks to InstanceConfigure selects + error alert (test instrumentation) 2026-06-06 11:37:03 -04:00
Joseph Doherty d33617d65d fix(host): register ActorSystem as DI singleton so health-probe scopes don't dispose it (HOST-021)
Per-probe health-check child scopes were disposing the AddTransient-bridged
ActorSystem (IDisposable), terminating the live cluster node ~4s after boot and
leaving every singleton-proxy Ask to hang the full 30s QueryTimeout — the central
report pages (/notifications, /site-calls, /monitoring/health) loaded in ~30s.
Bridge it as a singleton via a new lazy AkkaHostedService.GetOrCreateActorSystem()
so child-scope disposal never touches it. Verified: 0 post-startup terminates,
healthy active/standby, report pages ~0.05s, Playwright 68 passed / 0 failed.
2026-06-05 08:26:09 -04:00
Joseph Doherty 5ddb17a089 feat(auth): seed SCADA-Viewers->Viewer LDAP-group role mapping
Completes the multi-role test user's 4th role. HasData row Id=5
(SCADA-Viewers->Viewer) + the SCADA-Viewers group in the (now-retired) local
glauth config. The live shared dir is scadaproj/infra/glauth/.
2026-06-04 16:38:08 -04:00
Joseph Doherty 15752f8c2d fix(security): make auth cookie name configurable, override per env
The auth cookie name was hardcoded to ZB.MOM.WW.ScadaBridge.Auth. Because
browser cookies are scoped by host+path but NOT by port, two ScadaBridge
clusters on the same host (the local docker stack on localhost:9000 and
docker-env2 on localhost:9100) shared one cookie jar: signing into one
overwrote the other's cookie, and since the clusters use different JWT
signing keys + separate Data Protection key rings, the overwritten side
could no longer validate its cookie and the session died.

Add SecurityOptions.CookieName (default = canonical ZB.MOM.WW.ScadaBridge.Auth,
blank falls back to the default) applied via the SecurityOptions-bound cookie
PostConfigure. Override it to ...Auth.env2 in both docker-env2 Central nodes so
the two local clusters no longer collide; the primary cluster keeps the default
so its live sessions and production are unaffected. Adds 3 Security.Tests cases.
2026-06-03 13:11:29 -04:00
Joseph Doherty eabf270d71 docs: complete XML doc coverage (returns, summaries, inheritdoc)
Resolve all 622 issues flagged by the enhanced CommentChecker: add missing
<returns> tags (incl. the standard phrasing on non-generic Task methods),
add missing <summary> tags, and replace misused/redundant <inheritdoc/> on
members that override or implement nothing with real documentation.
Documentation-only — no behavior change; solution builds clean.
2026-06-03 11:39:32 -04:00
Joseph Doherty 9f18badf02 build(host): declare ZB.MOM.WW.Theme directly (not transitively via CentralUI)
Host/App.razor uses the kit's <ThemeHead/>/<ThemeScripts/>, but Host had no direct
PackageReference — it relied on CentralUI re-exporting the package transitively.
Add a versionless <PackageReference Include="ZB.MOM.WW.Theme"/> (version pinned by
central PM at Directory.Packages.props) so the declared dependency matches actual
usage and survives any future PrivateAssets/refactor on CentralUI. Additive only;
Host builds clean (0/0).
2026-06-03 04:52:00 -04:00
Joseph Doherty 837fb74ae5 chore(centralui): remove dead .sidebar shell CSS left by the theme cutover
The .sidebar/#sidebar-collapse/.nav-link/.nav-section-toggle block is orphaned —
the side rail is now the ZB.MOM.WW.Theme kit's .side-rail/.rail-link shell, and
no markup references these selectors. Kept the app-only #reconnect-modal and
.script-editor-modal rules (not provided by the kit). 95 lines removed; builds clean.
2026-06-03 04:37:23 -04:00
Joseph Doherty 58352a67cb fix(centralui): include AntiforgeryToken in LoginCard (match OtOpcUa + kit contract) 2026-06-03 03:39:47 -04:00
Joseph Doherty b9516e6721 feat(centralui): LoginCard sign-in
Replace hand-rolled Bootstrap card with the shared <LoginCard> from ZB.MOM.WW.Theme.
Update ComponentRenderingTests assertions to match LoginCard's rendered structure
(h1.login-title, div.panel.notice.login-error, "Sign in" button text).
2026-06-03 03:34:12 -04:00
Joseph Doherty 957203ec7b feat(centralui): MainLayout/NavMenu delegate to ZB.MOM.WW.Theme ThemeShell + kit nav 2026-06-03 03:31:10 -04:00
Joseph Doherty 6fb545d75b refactor(centralui): drop vendored theme.css/fonts/nav-state.js; keep app-only CSS in site.css 2026-06-03 03:25:04 -04:00
Joseph Doherty 6d75bdb372 feat(host): use ZB.MOM.WW.Theme ThemeHead + ThemeScripts 2026-06-03 03:23:03 -04:00
Joseph Doherty e1589497f1 build(centralui): reference ZB.MOM.WW.Theme 0.2.0 2026-06-03 03:21:44 -04:00
Joseph Doherty b3de8408fa feat(audit): ScadaBridge IAuditActorAccessor + wire audit Actor from Auth principal at authenticated emit sites (Phase 3) 2026-06-02 15:33:01 -04:00
Joseph Doherty bc0e5bfd37 docs(audit): ScadaBridge C7 review — correct 'six persisted' computed-col wording (5 persisted + IngestedAtUtc non-persisted) + stale perf iteration comment 2026-06-02 15:08:49 -04:00
Joseph Doherty 635461c0fd chore(audit): ScadaBridge C7 — perf re-baseline + CollapseAuditLogToCanonical projection test + index-test fix + dead-cref cleanup (Task 2.5)
Perf re-baseline (HotPathLatencyTests): empirical p95 on Apple M-series Release
build: 4KB DetailsJson slow path ≈14 µs, small-DetailsJson no-redactors ≈2 µs,
true no-op fast path ≈0 µs. Thresholds updated: 200 µs / 30 µs / 5 µs (≈15×
headroom for contested CI runners). Old thresholds (50 µs / 10 µs) were set for
the pre-C3 typed-field path; canonical JSON parse+rewrite is empirically faster.
Adds a third test (Filter_Apply_NoDetailsJson_FastPath) that asserts same-instance
return on the DetailsJson-null + within-cap fast path. Env-var overrides retained.

CollapseAuditLogToCanonicalMigrationTests (new): three MSSQL-gated [SkippableFact]
tests verifying Action/Category/Outcome projection, NULL Actor, DetailsJson codec
round-trip, and all six persisted computed columns (Kind/Status/SourceSiteId/
ExecutionId/ParentExecutionId) for ApiOutbound, InboundAuthFailure, and Failed-
status rows.

AddAuditLogTableMigrationTests: rename CreatesFiveNamedIndexes →
CreatesNineNamedIndexes; expand coverage from 5 original indexes to all 9 named
non-clustered indexes present after CollapseAuditLogToCanonical (adds
IX_AuditLog_Execution, IX_AuditLog_ParentExecution, IX_AuditLog_Node_Occurred,
UX_AuditLog_EventId).

Dead-cref cleanup: zero references to the deleted IAuditPayloadFilter /
DefaultAuditPayloadFilter / SafeDefaultAuditPayloadFilter types remain in any
.cs file (source or test). 26 occurrences across 13 files replaced with correct
references to IAuditRedactor / ScadaBridgeAuditRedactor / SafeDefaultAuditRedactor
or reworded as plain prose.

Residual sweep: no unused transitional code found beyond the acknowledged
"C3 transitional shim" comments on IngestedAtUtc stamping (active code, not dead).
2026-06-02 14:59:23 -04:00
Joseph Doherty 68a6bd1720 feat(audit)!: ScadaBridge C5 — collapse central dbo.AuditLog to 10 canonical cols + persisted computed cols; CollapseAuditLogToCanonical migration; repo writes canonical directly (Task 2.5) 2026-06-02 14:06:46 -04:00
Joseph Doherty 1737d15f04 fix(audit): ScadaBridge C4 review — enable PRAGMA foreign_keys + MarkForwarded state guard (no Reconciled demotion) + test (Task 2.5) 2026-06-02 13:23:36 -04:00
Joseph Doherty 946d3e2aef feat(audit): ScadaBridge C4 — site SQLite two-table (audit_event canonical + audit_forward_state sidecar), forwarding on sidecar, IsCachedKind drain split (Task 2.5) 2026-06-02 13:11:20 -04:00
Joseph Doherty c27b2c3d5f fix(audit): ScadaBridge C3 review — safe enum-parse (fallback) in SqliteAuditWriter.MapRow + AuditEventDtoMapper.FromDto (Task 2.5) 2026-06-02 12:55:07 -04:00
Joseph Doherty db707bb0de feat(audit)!: ScadaBridge C3 — swap to canonical ZB.MOM.WW.Audit.AuditEvent across seams/emitters/DTO/redactor wiring; transitional 24-col storage shim (Task 2.5) 2026-06-02 12:37:50 -04:00
Joseph Doherty 5aaf9e2923 fix(audit): ScadaBridge C2 review — over-redact scrubs all sensitive free-text fields + outer-catch never-leak test + marker alignment
I1 (security): OverRedact() in ScadaBridgeAuditRedactor now suppresses ErrorDetail,
ErrorMessage, and Extra (in addition to RequestSummary/ResponseSummary) to the
over-redacted marker in BOTH code paths (Deserialize+with path and the fallback
new-AuditDetails path). SafeDefaultAuditRedactor catch block aligned to match.

M3 (test): OuterCatch_OptionsThrows_NeverLeaks_AllSensitiveFieldsOverRedacted forces
the outer try/catch → OverRedact path via a ThrowingMonitor that throws from
CurrentValue (the first statement in the try block). Asserts (a) Apply does not
throw, and (b) all five sensitive free-text fields are suppressed to the
over-redacted marker with PayloadTruncated=true.

M1 (consistency): SafeDefaultAuditRedactor now uses AuditRedactionPrimitives
constants (RedactedMarker for line-format header values, OverRedactedEventMarker
for the catch block), eliminating the divergent [REDACTED]/[redacted by ...]
strings. AuditRedactionPrimitives gains OverRedactedEventMarker = RedactorErrorMarker.
SafeDefaultAuditRedactorTests updated from [REDACTED] → <redacted>.

M2 (comment): Added one-line note in TruncateField explaining why the char-count
(result.Length != value.Length) truncation check is sufficient given TruncateUtf8
only ever shortens.
2026-06-02 11:12:18 -04:00
Joseph Doherty adfb4d385c feat(audit): ScadaBridge C2 — ScadaBridgeAuditRedactor/SafeDefaultAuditRedactor : IAuditRedactor on canonical record (Task 2.5) 2026-06-02 11:00:36 -04:00
Joseph Doherty 3d77dc003c feat(audit): ScadaBridge C1 — AuditDetails codec (deterministic) + AuditOutcome projection + canonical field builders + ZB.MOM.WW.Audit ref (Task 2.5)
Additive foundation only — no existing type/interface/emitter changed.
Commons now references ZB.MOM.WW.Audit 0.1.0 (Gitea feed, central PM pin).
Adds four pure new types in Commons/Types/Audit/:
  AuditDetails (sealed record, 17 domain fields, declaration-order = JSON key order)
  AuditDetailsCodec (static; single cached JsonSerializerOptions: camelCase, no-indent,
    WhenWritingNull, UnsafeRelaxedJsonEscaping — byte-deterministic across calls)
  AuditOutcomeProjector (static; InboundAuthFailure→Denied first, then Delivered→Success,
    Failed/Parked/Discarded→Failure, all others→Success)
  AuditFieldBuilders (static; BuildAction="{channel}.{kind}", BuildCategory=channel.ToString())
56 new tests in Commons.Tests/Types/Audit/ covering codec round-trip, byte-determinism
(hand-pinned expected JSON string), null/empty sentinel, full projection table,
InboundAuthFailure-Denied precedence, and Action/Category builders. All pass.
2026-06-02 10:42:51 -04:00
Joseph Doherty 4118452e72 docs(auth): ScadaBridge Task 1.7 review — correct stale role-name prose in NavMenu comments (Admin/Design/Deployment/Audit→canonical) 2026-06-02 08:13:38 -04:00
Joseph Doherty b104760b3a feat(auth)!: ScadaBridge canonical roles + SoD collapse (Audit→Administrator, AuditReadOnly→Viewer) + config-DB migration (Task 1.7)
Standardize role string VALUES on the canonical vocabulary
(Administrator/Designer/Deployer/Viewer; Operator/Engineer unused here):
  Admin        -> Administrator
  Design       -> Designer
  Deployment   -> Deployer
  Audit        -> Administrator   (COLLAPSE; accepted privilege escalation)
  AuditReadOnly-> Viewer          (COLLAPSE; keeps audit-read, no export)

SoD: OperationalAuditRoles = { Administrator, Viewer },
     AuditExportRoles      = { Administrator }
so Viewer reads the audit log + nav but cannot bulk-export, while
Administrator does both + holds the full admin surface (the documented,
accepted auditor/admin SoD collapse).

Atomic move across every enforcement site:
- Roles constants; AuthorizationPolicies (RequireClaim values + SoD arrays +
  honest XML-doc); RoleMapper Deployer check.
- ManagementActor.GetRequiredRole switch + the hard-coded site-scope
  admin-bypass (now Roles.Administrator at all 6 sites). Site-scoping logic
  is otherwise unchanged.
- DebugStreamHub Administrator/Deployer gates (Deployer kept case-sensitive).
- CentralUI BrowseService/BindingTester Designer guards; LdapMappingForm
  dropdown now offers canonical values (incl. Viewer).
- Config-DB seed (LdapGroupMappings Id 1-4) + EF migration CanonicalizeRoles:
  Id-keyed UpdateData for seed rows + idempotent raw catch-all UPDATEs for
  operator-added rows. Down is lossy on the collapse (documented in-file).
  No pending model changes.

Tests reworked to the collapsed model across Security/CentralUI/
ManagementService/ConfigurationDatabase/Integration suites, incl. explicit
Viewer-reads-not-exports and former-Audit-now-Administrator-escalation cases.

CHANGELOG: BREAKING security note documenting the canonicalization + SoD
collapse.
2026-06-02 08:00:47 -04:00
Joseph Doherty 6ae605160c chore(auth): ScadaBridge unify dev LDAP base DN to dc=zb,dc=local (Task 1.6)
Replace dc=scadabridge,dc=local with dc=zb,dc=local in all dev/test LDAP
references — app config, docker test-cluster node configs (docker/ and
docker-env2/), GLAuth fixture, dev tooling, Host.Tests fixtures,
IntegrationTests factory, and operational test_infra docs. OU structure
(ou=SCADA-Admins,ou=users,etc.) preserved throughout. Email domains
(@scadabridge.local), hostnames, and container names are untouched.
Historical plan docs (2026-05-24-second-environment.md,
2026-05-31-folder-repo-rename-scadabridge-design.md) excluded as
point-in-time records. No synthetic dc=example,dc=com placeholders touched.
2026-06-02 06:54:14 -04:00
Joseph Doherty a0938f708b feat(auth): ScadaBridge full canonical claims (ZbClaimTypes role/scope) + ZbCookieDefaults, keep cookie name (Task 1.5) 2026-06-02 06:23:15 -04:00
Joseph Doherty afa55981d5 feat(auth)!: ScadaBridge retire SQL Server ApiKey entity + ApprovedApiKeyIds + legacy hashing; EF migration RetireInboundApiKeyStore; re-issue runbook + CHANGELOG (re-arch C5/E) — BREAKING: X-API-Key -> Bearer sbk_, keys re-issued 2026-06-02 05:39:59 -04:00
Joseph Doherty b13d7b3d28 fix(auth): C4 review polish — document backward-compat JSON tolerance, shared BundleJsonOptions, PreviewAsync legacy-bundle test, doc fix (review I-2/I-3/M-1/M-2; I-1 intentionally skipped) 2026-06-02 05:15:50 -04:00
Joseph Doherty 731cfd3bfc feat(auth): ScadaBridge TransportExport excludes inbound API keys (re-arch C4; methods-only, import ignores legacy key sections); keys re-issued per environment 2026-06-02 05:06:40 -04:00
Joseph Doherty d1191fddf9 fix(auth): C3 review — surface seam not-found (no silent success), partial-reconcile-failure guidance, create validation order, concurrent-edit reconciler test 2026-06-02 04:46:32 -04:00
Joseph Doherty 107e524914 feat(auth): ScadaBridge CentralUI pages onto IInboundApiKeyAdmin seam (re-arch C3; string keyId, method-scopes replace ApprovedApiKeyIds, token-once display, approved-keys<->scopes inversion) 2026-06-02 04:36:50 -04:00
Joseph Doherty 8219b8ee18 fix(auth): C2 review — not-found throws (no spurious audit) on update/delete/set-methods, reject empty methods (unusable-key/stealth-disable), richer set-methods response, token advisory to stderr 2026-06-02 04:21:28 -04:00
Joseph Doherty 6518e93424 feat(auth): ScadaBridge ManagementActor + CLI + Commons messages onto IInboundApiKeyAdmin seam (re-arch C2; int->string keyId, +Methods, +SetApiKeyMethods) 2026-06-02 04:11:44 -04:00
Joseph Doherty 7f7ea3f3c9 fix(auth): C1 review polish — guard name at seam, document seam contract (throws/O(n)), explicit cookie test (review #1/#2/#3/#5/#8) 2026-06-02 04:01:43 -04:00
Joseph Doherty 55099b19f6 fix(auth): move AddZbLdapAuth to Host composition root so component-lib AddSecurity() drops IConfiguration param (satisfy OptionsTests arch rule; fix pre-existing ac34dac red); behaviour-preserving 2026-06-02 03:50:16 -04:00
Joseph Doherty d09def2be0 feat(auth): ScadaBridge re-pin Auth 0.1.3 + add IInboundApiKeyAdmin seam over library admin facade (re-arch C1, additive) 2026-06-02 03:32:25 -04:00