diff --git a/docker-dev/docker-compose.yml b/docker-dev/docker-compose.yml index 89b9af91..924f1bfb 100644 --- a/docker-dev/docker-compose.yml +++ b/docker-dev/docker-compose.yml @@ -522,6 +522,12 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # #520 stage 6 — node ACL enforcement, the dark switch. Default OFF everywhere, exactly as in + # production; the site-a pair is the demo/live-gate subject, so it reads an env override: + # OTOPCUA_ACL_ENABLED=true docker compose -f docker-dev/docker-compose.yml up -d site-a-1 site-a-2 + # With it on, EVERY data-plane operation needs a matching NodeAcl grant — a principal with no + # grant connects and sees nothing. site-b stays hardcoded OFF as the A/B control. + Security__Acl__Enabled: "${OTOPCUA_ACL_ENABLED:-false}" # Per-cluster mesh Phase 5/6 (gRPC live-telemetry stream) — see central-1/central-2. This # node carries cluster-SITE-A + driver, so SplitTopologyTransportValidator requires # Telemetry:Mode=Grpc (mandatory, not a dark switch, as of Phase 6). The dedicated :4056 @@ -631,6 +637,12 @@ services: ConfigSource__CentralFetchEndpoints__0: "http://central-1:4055" ConfigSource__CentralFetchEndpoints__1: "http://central-2:4055" ConfigSource__ApiKey: "configserve-docker-dev-key" + # #520 stage 6 — node ACL enforcement, the dark switch. Default OFF everywhere, exactly as in + # production; the site-a pair is the demo/live-gate subject, so it reads an env override: + # OTOPCUA_ACL_ENABLED=true docker compose -f docker-dev/docker-compose.yml up -d site-a-1 site-a-2 + # With it on, EVERY data-plane operation needs a matching NodeAcl grant — a principal with no + # grant connects and sees nothing. site-b stays hardcoded OFF as the A/B control. + Security__Acl__Enabled: "${OTOPCUA_ACL_ENABLED:-false}" # Per-cluster mesh Phase 5/6 (gRPC live-telemetry stream) — see central-1/site-a-1. # Telemetry:Mode=Grpc is mandatory as of Phase 6 (cluster-SITE-A + driver); the dedicated # :4056 listener binds regardless. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 13d733fa..ecbdc79c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -2754,8 +2754,11 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 if (source is not null && !Permits(identity, OpcUaOperation.Browse, source)) throw new ServiceResultException(StatusCodes.BadUserAccessDenied); + // Only the references THIS call appends are ours to filter — on a BrowseNext the list already + // carries the earlier pages, and re-examining them would both waste work and skew the summary. var before = references.Count; base.Browse(context, ref continuationPoint, references); + var returned = references.Count - before; var filtered = 0; for (var ii = references.Count - 1; ii >= before; ii--) @@ -2773,7 +2776,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 filtered++; } - LogAclFiltering("Browse", identity, filtered, references.Count + filtered); + LogAclFiltering("Browse", identity, filtered, returned); } ///