Commit Graph

3466 Commits

Author SHA1 Message Date
Joseph Doherty d79d51dd4f Merge branch 'fix/r3-01-mesh-grpc-allowanonymous' — R3-01: mesh gRPC + LocalDb sync endpoints opt out of the AdminUI cookie FallbackPolicy (PSK interceptors are the gate); real-auth-stack boundary tests + map-convention guard; rig DisableLogin parameterised (archreview 03/S14 Critical) 2026-08-20 10:43:49 -04:00
Joseph Doherty c8cf5c8032 test(host): the R3-01 falsifiability control asserts the cookie-challenge signature (a bare Exception was satisfiable by an anonymised endpoint too); docs wording 2026-08-20 10:43:49 -04:00
Joseph Doherty 851e4cf5e0 Merge branch 'fix/r3-03-restart-resubscribe-watchdog' — R3-03: SpawnChild always sends the desired set (operator Restart + supervision restart re-provision, stale-alarm-set re-subscribe, incarnation check, 10 s backoff); watchdog grace = max(LastSuccessfulRead, connectedSince) (archreview 03/S16, S17, U6) 2026-08-20 10:40:11 -04:00
Joseph Doherty 6d0002889a docs(archreview): R3-03 review outcome — F1/F2/F3 fixed with red-before evidence 2026-08-20 10:39:47 -04:00
Joseph Doherty 0793b0cba8 fix(runtime): rate-limit the supervision reprovision — deferred, not dropped (review F3)
The default decider restarts a faulting child immediately and without limit, and
the hook makes every restart do ShutdownAsync+InitializeAsync+SubscribeAsync, so a
driver faulting on every value became a reconnect storm. Per-driver last-reprovision
stamp + DefaultReprovisionBackoff (10s, matched to DefaultReconnectInterval);
a held request is re-delivered by a single per-driver timer carrying the original
incarnation ref, so the F2 identity check still applies.

Seen RED with the backoff disabled: InitializeCount == 3 (second applied
immediately) where the test requires 2-then-3.

Also: the _children.Keys union-arm test (seeder gained taglessDrivers) and the
_desiredByDriver xmldoc note that it lingers on last-known-good after a failed apply.
2026-08-20 10:39:13 -04:00
Joseph Doherty b725788954 docs(plans): R3-01 execution notes (T1-T9) - filter gotcha, T4/T5 fixture scope, observed cookie-challenge signature 2026-08-20 10:28:55 -04:00
Joseph Doherty b8911eaa14 docs(plans): mark R3-01 T9 done; T10 remains pending (live gate, orchestrator-run) 2026-08-20 10:27:45 -04:00
Joseph Doherty a222421374 docs: mesh gRPC ingress is AllowAnonymous; the PSK interceptors are the gate (R3-01)
New docs/security.md section "Mesh gRPC ingress is AllowAnonymous by design"
(archreview 03/S14): what the cookie FallbackPolicy covers, why it can block a
correctly-PSK'd call before the interceptor's verdict matters, why the
AllowAnonymous opt-out is safe (all five interceptors fail-closed), the
affected-endpoint table (fused-vs-driver-only distinction), and the two
standing guards (MeshGrpcAnonymousMetadataTests, MeshGrpcMapConventionTests)
plus the auth-stack/driver-only boundary tests.

One cross-reference line each in docs/Configuration.md (§MeshTransport,
§ConfigSource/ConfigServe) and docs/Telemetry.md, so an operator reading
about the listener ports meets the constraint where they'd look for it.
2026-08-20 10:27:40 -04:00
Joseph Doherty 2b7100cf19 fix(runtime): route a reprovision by incarnation, not driver id (review F2)
A ReprovisionRequested in flight from an old incarnation could reach the NEW child
after HandleRestartDriver swapped it in. In Connecting that child DOES handle
InitializeRequested, so it started a second concurrent InitializeAsync, racing the
#527 dispose-before-overwrite.

Seen RED: InitializeCount == 2 on a child held in Connecting by a gated init
(an already-connected child swallows InitializeRequested and passes vacuously).
2026-08-20 10:27:02 -04:00
Joseph Doherty 4dde5cb8bf docs(plans): mark R3-01 T7 and T8 done 2026-08-20 10:25:12 -04:00
Joseph Doherty f3832aa48c test(host): a mesh gRPC map added outside MeshGrpcEndpoints fails the build
MeshGrpcMapConventionTests - source-scan of Program.cs (solution-root walk,
following FlushLoggingConventionTests.cs's idiom): zero direct
MapGrpcService</MapZbLocalDbSync( occurrences, exactly one
MapOtOpcUaMeshGrpcServices( call.

Complements the endpoint-metadata census (T1-T3): the census catches a map
added to MeshGrpcEndpoints without the AllowAnonymous convention, but cannot
see a map that bypasses the helper and goes straight into Program.cs - that
shape never reaches MapOtOpcUaMeshGrpcServices at all. Verified red against
the pre-T1 Program.cs (five inline map sites, zero mapper calls) and green
against the current extracted form.
2026-08-20 10:25:02 -04:00
Joseph Doherty edd01fcc34 fix(runtime): a changed alarm set re-subscribes instead of latching (review F1)
The alarm subscribe gated on 'a handle exists', which is correct for an idempotent
re-push and wrong for a deploy that widens/narrows the set. Combined with the
spawn-time desired set, a respawning deploy could latch the PREVIOUS revision's
alarm refs for the life of the connection; sourceNodeIds-filtering drivers
(OpcUaClient/AbCip/Focas) would never deliver the new alarm.

Seen RED: LastSubscribedAlarmRefs stayed [alarmX] after a deploy widening to
[alarmX, alarmY].
2026-08-20 10:24:52 -04:00
Joseph Doherty 82ad98e1a9 chore(docker-dev): parameterise Security__Auth__DisableLogin on the central pair
Security__Auth__DisableLogin: "${OTOPCUA_DISABLE_LOGIN:-true}" on central-1
and central-2 - byte-identical default behaviour, verified via `docker
compose config` both unset (resolves "true") and with
OTOPCUA_DISABLE_LOGIN=false (resolves "false").

DisableLogin's always-succeeding handler, registered under the cookie scheme
name, masks the AdminUI cookie FallbackPolicy entirely - which is exactly how
03/S14 (the FallbackPolicy blocking every mesh gRPC call on a login-enabled
admin node) survived seven phases of prior live gates that all ran with the
default. The production-faithful mode is now a one-variable flip instead of
an edit-and-remember-to-revert; header comment names the flip and the LDAP
users it needs (archreview 03/S14, R3-01 T8).
2026-08-20 10:24:49 -04:00
Joseph Doherty 9bd3366b28 docs(plans): mark R3-01 T6 done 2026-08-20 10:21:01 -04:00
Joseph Doherty 306ea1aecf test(host): driver-only nodes carry no authorization middleware (03/S14 exemption pinned)
MeshGrpcDriverOnlyPipelineTests - the driver-role shape (no AddOtOpcUaAuth, no
UseAuthentication/UseAuthorization at all) behaves identically to the fused
shape's T4 positive/negative tests for NodeCommandService and
TelemetryStreamService: a correctly-keyed call succeeds, a no-key call gets
the interceptor's PermissionDenied + detail string.

Also asserts the STRUCTURAL reason it is unaffected - no
IAuthorizationPolicyProvider registered at all - so a future "register auth
everywhere for consistency" change fails this test before it ever reaches a
live site outage. Confirmed (by temporarily swapping in the pre-T2/T3 mapper)
that this class passes identically on both builds: driver-only nodes were
never exposed to 03/S14 in the first place, with or without
.AllowAnonymous().
2026-08-20 10:20:56 -04:00
Joseph Doherty e475f7b4ab Merge branch 'fix/r3-05-acl-hot-path-expiry' — R3-05: bounded ConfigDb API-key lookup (deadline + TTL) and clock-driven key-expiry revalidation of live monitored items (archreview 03/S18, 03/S19) 2026-08-20 10:19:13 -04:00
Joseph Doherty 62515f3279 fix(security): pin the cached-key expiry guard, meter cache_hit after it, and close two wiring gaps (review fixes) 2026-08-20 10:18:52 -04:00
Joseph Doherty 353ed6bb58 docs(plans): mark R3-01 T5 done 2026-08-20 10:15:03 -04:00
Joseph Doherty 8471db4b1d test(host): no-PSK mesh calls report the interceptor's verdict, not a cookie 401/redirect
Extends MeshGrpcAuthStackTests (T5): a no-bearer or wrong-bearer call to
DeploymentArtifactService.Fetch, TelemetryStreamService.Subscribe and
NodeCommandService.Execute fails with the INTERCEPTOR's detail string
("Config-serve authentication failed.", "Telemetry stream authentication
failed.", "Node command authentication failed."), asserted alongside the
status code rather than in place of it - the vacuity trap the plan calls out
(ConfigServeAuthInterceptor throws Unauthenticated, and Grpc.Net.Client can
map an HTTP 401 to the same status, so status alone would not discriminate a
broken build).

Verified red-then-green against the T1 (pre-AllowAnonymous) mapper: all 6 new
theory cases fail there with StatusCode.Internal ("HTTP/2 server reset the
stream, PROTOCOL_ERROR") instead of Unauthenticated/PermissionDenied - the
cookie challenge breaking the h2c stream before any gRPC trailers are
written, never a clean auth denial. That is the exact signature the live
gate (T10) should grep for; recorded in the class remarks.
2026-08-20 10:14:55 -04:00
Joseph Doherty c1ad9e24d2 docs(archreview): R3-03 execution notes — final verification + deviation summary 2026-08-20 10:11:16 -04:00
Joseph Doherty 815423bd88 docs(plans): restore literal UTF-8 em-dashes in R3-01 tasks.json (cosmetic) 2026-08-20 10:07:20 -04:00
Joseph Doherty 12c4028474 docs: restart re-provisioning + per-connection watchdog grace (R3-03) 2026-08-20 10:07:20 -04:00
Joseph Doherty af9d3a69ac docs(plans): mark R3-01 T4 done 2026-08-20 10:06:55 -04:00
Joseph Doherty 458c84c34f test(host): mesh gRPC boundary under the production auth stack (FallbackPolicy live)
MeshGrpcAuthStackTests - the CI-shaped guard 03/S14 asked for. A real
AddOtOpcUaAuth(config) + UseAuthentication/UseAuthorization pipeline (cookie
scheme, DisableLogin=false, EnvironmentName=Production) with the production
interceptor set, driving DeploymentArtifactService.Fetch,
CentralCommandService.ReportApply and NodeCommandService.Execute through
MapOtOpcUaMeshGrpcServices.

Verified red-then-green by hand: with the T1 (pre-AllowAnonymous) mapper
swapped in, all three positive calls fail with
"HTTP/2 server reset the stream, PROTOCOL_ERROR" (RpcException/Internal) -
the cookie challenge breaking prior-knowledge h2c, not a clean auth denial.
With the current mapper (T2/T3) all three succeed. That asymmetry is the only
thing distinguishing the two builds; every other assertion here (cookie
handler identity, the falsifiability control) is unchanged by the fix and
proves nothing on its own.

Falsifiability control: LocalDbSyncService is mapped directly (bypassing
MapOtOpcUaMeshGrpcServices) with no .AllowAnonymous(), and a correctly-shaped
call to it is refused - proving the fixture's auth stack is genuinely live,
not accidentally permissive.
2026-08-20 10:06:47 -04:00
Joseph Doherty 14094e3a18 docs(archreview): file R3-05 residuals R1/R2 (Gitea #562/#563); execution notes 2026-08-20 10:06:00 -04:00
Joseph Doherty 5c7e5725a5 test(runtime): first supervision-restart test — child re-initialises + re-subscribes (archreview 03/U6)
Crash seam: the planned null-Snapshot OnDataChange (no SupervisorStrategy fallback needed).
Seen RED with T6 reverted: InitializeCount 1, not 2 — with a preceding ShutdownCount>=1
assertion proving the restart really happened, so the red is not ambiguous with 'never crashed'.
2026-08-20 10:05:45 -04:00
Joseph Doherty a9b592aeb8 docs(security): correct revocation latency (activation vs open session) + S18/S19 knobs 2026-08-20 10:04:03 -04:00
Joseph Doherty dfab7f614d test(integration): key expiry stops a live subscription over the wire, with no deploy 2026-08-20 10:00:52 -04:00
Joseph Doherty 2b9a397b2d fix(runtime): re-provision a driver child after a supervision restart (archreview 03/S6, S16) 2026-08-20 09:56:05 -04:00
Joseph Doherty a4f12bccef fix(host): run monitored-item revalidation on a low-frequency timer (archreview 03/S19) 2026-08-20 09:55:01 -04:00
Joseph Doherty d97e441979 test(runtime): operator restart re-subscribes the respawned child (archreview 03/S16, U6)
Seen RED against a T3-reverted build: all 4 failed, Created[1].SubscribeCount == 0
on the headline case. 20x isolation run green after the fix was restored.
2026-08-20 09:53:38 -04:00
Joseph Doherty 1759f60476 docs(plans): mark R3-01 T3 done 2026-08-20 09:51:43 -04:00
Joseph Doherty e3cf363703 fix(host): LocalDb sync endpoint opts out of the cookie fallback via an empty-prefix anon group
MapZbLocalDbSync returns IEndpointRouteBuilder, not an IEndpointConventionBuilder,
so .AllowAnonymous() cannot be chained onto it the way the other four mesh maps
are (T2). app.MapGroup(string.Empty).AllowAnonymous().MapZbLocalDbSync() carries
the same convention while leaving the gRPC route pattern byte-identical -
verified directly (LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical).

The census's RealMethodEndpoints filter had to move off a "route pattern starts
with /" heuristic to an exact-route-pattern-set match: inside the empty-prefix
group, grpc-dotnet's own "Unimplemented service"/"Unimplemented method" fallback
endpoints also gain a leading "/", so the heuristic let them back in and
over-counted (caught by DriverOnly_WithASyncPort_AlsoMapsTheLocalDbSyncEndpoint
and LocalDbSync_AllowAnonymousGroup_LeavesTheRoutePatternByteIdentical going red
against the naive filter before this fix).

All five mesh gRPC endpoints now carry IAllowAnonymous (archreview 03/S14, T3).
2026-08-20 09:51:33 -04:00
Joseph Doherty b09f78396f test(opcua): pin clock-driven expiry in the revalidation pass; correct the only-trigger remark 2026-08-20 09:50:39 -04:00
Joseph Doherty bd80ff96ca feat(security): AclRevalidationTimer — clock-driven monitored-item revalidation (archreview 03/S19) 2026-08-20 09:46:56 -04:00
Joseph Doherty 2f90156408 docs(plans): mark R3-01 T2 done 2026-08-20 09:46:40 -04:00
Joseph Doherty b05ebd6353 fix(host): mesh gRPC endpoints opt out of the AdminUI cookie FallbackPolicy (archreview 03/S14)
.AllowAnonymous() on DeploymentArtifactGrpcService, CentralCommandGrpcService,
TelemetryStreamGrpcService and NodeCommandGrpcService — the four MapGrpcService
maps that are chainable. On a login-enabled admin node the global cookie
FallbackPolicy (RequireAuthenticatedUser) applies to every endpoint without
explicit authorization metadata, gRPC included; the PSK bearer interceptors run
INSIDE the endpoint invocation, strictly after that middleware, so a
correctly-keyed node call was challenged before the interceptor's verdict could
matter. Safe because all four interceptors fail closed on an unset key.

The census now asserts IAllowAnonymous on all four (red before this change,
green after) plus a scope guard proving the opt-out never leaks onto an
unrelated endpoint. MapZbLocalDbSync (T3) is not chainable the same way and is
asserted absent here on purpose, so this test cannot start passing for the
wrong reason once T3 lands.
2026-08-20 09:46:30 -04:00
Joseph Doherty 39a10a277a fix(security): wrap the ConfigDb key store on admin nodes + registration guard 2026-08-20 09:45:08 -04:00
Joseph Doherty 6048257dfe feat(observability): meter bounded API-key store lookups 2026-08-20 09:43:07 -04:00
Joseph Doherty ac9a71bcbd docs(plans): mark R3-01 T1 done 2026-08-20 09:43:06 -04:00
Joseph Doherty 6dab72a479 refactor(host): extract MapOtOpcUaMeshGrpcServices (one authority for the five mesh maps)
Behaviour-preserving: Program.cs:743-784 collapses to a single call, same gates,
same order, every existing per-map rationale comment carried over. A new
endpoint-metadata census (MeshGrpcAnonymousMetadataTests) drives the SAME
production mapper across all three role shapes (admin-only / driver-only /
fused), so the guard can never assert a shape production no longer has -
archreview 03/S14, T1.
2026-08-20 09:42:49 -04:00
Joseph Doherty b3a71f6655 test(runtime): recording subscribable driver factory + enabled-driver v3 seed 2026-08-20 09:42:33 -04:00
Joseph Doherty 37288d4df5 feat(security): short TTL read-through cache in front of the ConfigDb key store 2026-08-20 09:40:56 -04:00
Joseph Doherty 3b56e5bd01 fix(security): bound the API-key store call under the SessionManager lock (archreview 03/S18) 2026-08-20 09:38:41 -04:00
Joseph Doherty daaeb854d6 fix(runtime): every spawn hands the child its desired subscriptions (archreview 03/S16) 2026-08-20 09:36:52 -04:00
Joseph Doherty 894444376e feat(security): register TimeProvider + S18/S19 option knobs (archreview 03/S18,S19) 2026-08-20 09:36:14 -04:00
Joseph Doherty bdc81a1372 refactor(runtime): one desired-subscription payload authority (_desiredByDriver) 2026-08-20 09:34:49 -04:00
Joseph Doherty 72329eabdd docs(archreview): round-3 plan R3-02 (stale-Good family — one quality contract, four seams); index task count 2026-08-20 09:33:36 -04:00
Joseph Doherty a801e2aefb fix(runtime): comms-loss grace is per connection — max(LastSuccessfulRead, connectedSince) (archreview 03/S17) 2026-08-20 09:32:30 -04:00
Joseph Doherty dd7f495432 docs(archreview): round-3 plan R3-05 (ACL hot-path deadline + clock-driven key expiry); index task count 2026-08-20 09:28:53 -04:00