fix(sphistorianclient): gRPC auth handshake uses StorageService.ValidateClientCredential

The RemoteGrpc orchestrator drove the SSPI/NTLM token loop through
HistoryService.ExchangeKey, which the 2023 R2 contract analysis shows is a
separate key-exchange/cert op — not the credential handshake. The server
rejected the NTLM Type-1 token at round 0. The Negotiate loop belongs on
StorageService.ValidateClientCredential (Handle/InBuff -> Status/OutBuff;
field names match the 2020 native contract). Live-verified end-to-end against
a 2023 R2 Historian (wonder-sql-vd03): SysTimeSec raw read returns correct
timestamped values.
This commit is contained in:
Joseph Doherty
2026-06-19 06:56:44 -04:00
parent 5f7d7e1b58
commit a0527f9b5a
@@ -5,6 +5,7 @@ using ZB.MOM.WW.SPHistorianClient.Models;
using ZB.MOM.WW.SPHistorianClient.Wcf; using ZB.MOM.WW.SPHistorianClient.Wcf;
using GrpcHistory = ArchestrA.Grpc.Contract.History; using GrpcHistory = ArchestrA.Grpc.Contract.History;
using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval; using GrpcRetrieval = ArchestrA.Grpc.Contract.Retrieval;
using GrpcStorage = ArchestrA.Grpc.Contract.Storage;
namespace ZB.MOM.WW.SPHistorianClient.Grpc; namespace ZB.MOM.WW.SPHistorianClient.Grpc;
@@ -16,17 +17,19 @@ namespace ZB.MOM.WW.SPHistorianClient.Grpc;
/// ///
/// Operation mapping (2020 WCF → 2023 R2 gRPC): /// Operation mapping (2020 WCF → 2023 R2 gRPC):
/// Hist.GetInterfaceVersion → HistoryService.GetInterfaceVersion /// Hist.GetInterfaceVersion → HistoryService.GetInterfaceVersion
/// Hist.ValidateClientCredential (loop) → HistoryService.ExchangeKey (loop) /// Hist.ValidateClientCredential (loop) → StorageService.ValidateClientCredential (loop)
/// Hist.OpenConnection2 → HistoryService.OpenConnection /// Hist.OpenConnection2 → HistoryService.OpenConnection
/// Retr.StartQuery2 → RetrievalService.StartQuery /// Retr.StartQuery2 → RetrievalService.StartQuery
/// Retr.GetNextQueryResultBuffer2 (loop) → RetrievalService.GetNextQueryResultBuffer (loop) /// Retr.GetNextQueryResultBuffer2 (loop) → RetrievalService.GetNextQueryResultBuffer (loop)
/// Retr.EndQuery2 → RetrievalService.EndQuery /// Retr.EndQuery2 → RetrievalService.EndQuery
/// ///
/// NOTE: not yet live-verified against a 2023 R2 server. The auth handshake uses /// AUTH: the SSPI/Negotiate token loop runs through StorageService.ValidateClientCredential
/// HistoryService.ExchangeKey because the gRPC HistoryService dropped ValidateClientCredential /// (Handle + InBuff → Status + OutBuff) — per the 2023 R2 contract analysis, that op carries the
/// (it now lives only on StorageService) and gained ExchangeKey with the identical /// NTLM/SSPI tokens (the field names inBuff/outBuff match the 2020 native contract), whereas
/// handle+token→token shape. If a live server rejects this, the handshake op is the first thing /// HistoryService.ExchangeKey is a separate key-exchange/cert op (NOT the credential handshake).
/// to revisit — everything else is the proven 2020 byte protocol. /// OpenConnection and the retrieval chain stay on their original services; the server correlates
/// the validated context by the handshake GUID handle. Live-verified 2026-06-19 against a 2023 R2
/// server (wonder-sql-vd03) — earlier ExchangeKey wiring was rejected at token round 0.
/// </summary> /// </summary>
internal sealed class HistorianGrpcReadOrchestrator internal sealed class HistorianGrpcReadOrchestrator
{ {
@@ -162,18 +165,19 @@ internal sealed class HistorianGrpcReadOrchestrator
{ {
Guid contextKey = Guid.NewGuid(); Guid contextKey = Guid.NewGuid();
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel); var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel);
historyClient.GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken); historyClient.GetInterfaceVersion(new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken);
HistorianNativeHandshake.RunTokenRounds( HistorianNativeHandshake.RunTokenRounds(
(handle, wrapped, _) => (handle, wrapped, _) =>
{ {
GrpcHistory.ExchangeKeyResponse response = historyClient.ExchangeKey( GrpcStorage.ValidateClientCredentialResponse response = storageClient.ValidateClientCredential(
new GrpcHistory.ExchangeKeyRequest { StrHandle = handle, BtInput = ByteString.CopyFrom(wrapped) }, new GrpcStorage.ValidateClientCredentialRequest { Handle = handle, InBuff = ByteString.CopyFrom(wrapped) },
connection.Metadata, connection.Metadata,
Deadline(), Deadline(),
cancellationToken); cancellationToken);
byte[] serverOutput = response.BtOutput?.ToByteArray() ?? []; byte[] serverOutput = response.OutBuff?.ToByteArray() ?? [];
byte[] error = response.Status?.BtError?.ToByteArray() ?? []; byte[] error = response.Status?.BtError?.ToByteArray() ?? [];
bool success = response.Status?.BSuccess ?? false; bool success = response.Status?.BSuccess ?? false;
return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error); return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error);