fix(security): scope operator-authored credential references per driver (#815)

A Designer could point a Galaxy / OpcUaClient endpoint at a host they control,
set a credential field to any reference the node can resolve
(env:ConnectionStrings__ConfigDb, env:ServerHistorian__ApiKey, secret:<any>,
Galaxy file:<any path>) and click Test Connect / live browse; the node resolved
the value and sent it to that host.

One shared rule in Core.Abstractions (CredentialReferences +
DriverCredentialPolicy) generalises the #803 Heidenhain namespace:
- Galaxy: secret:galaxy/... | secret:otopcua/galaxy/... | env:OTOPCUA_GALAXY_... |
  legacy env:GALAXY_MXGW_...; file: refused; dev:/literal API key unchanged.
- OpcUaClient: secret:opcuaclient/... | env:OTOPCUA_OPCUACLIENT_...; literals unchanged.
- Heidenhain adopts the shared parser/resolver (namespace unchanged).
Enforced at the AdminUI save path (DriverConfigSaveGate), the deploy gate
(CredentialReferenceDeployChecks, code CredentialReferenceOutOfScope), Test
Connect (AdminOperationsActor, before any probe), live browse
(BrowserSessionService, before any browser) and again at resolve time in every
driver/browser (fail closed, before any store/env read).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-11-11 01:28:58 -05:00
parent 5bbe1e3c7a
commit a224fd6391
23 changed files with 1252 additions and 314 deletions
@@ -585,6 +585,20 @@ public sealed class AdminOperationsActor : ReceiveActor
return;
}
// lmxopcua#815: refuse a credential reference outside the driver's namespace BEFORE any probe runs — the
// probe would otherwise be the seam that resolves it and sends it to the author-chosen endpoint. Nothing
// is read from the secret store or the environment on this path.
var credentialProblems = DriverCredentialPolicy.DescribeProblems(msg.DriverType, msg.ConfigJson);
if (credentialProblems.Count > 0)
{
replyTo.Tell(new TestDriverConnectResult(
false,
"Test Connect refused: " + string.Join(" ", credentialProblems),
null,
msg.CorrelationId));
return;
}
Task.Run(() => ProbeAsync(probe, msg)).PipeTo(replyTo);
}