feat(opcuaclient-browser): real AttributesAsync + certificate-auth browse (R06:O-5)
- AttributesAsync was a stub returning nothing; it now reads NodeClass / DisplayName / DataType / ValueRank / UserAccessLevel in one request and maps them through UpstreamAttributeMapping, the same mapping discovery registers with (moved into the shared .Session leaf; the driver forwards to it). Objects have no value attributes. - Certificate auth was refused "in v1". The browse now builds its identity through OpcUaUserIdentityFactory (shared .Session leaf, the driver forwards to it), including the secret: resolution of Password / UserCertificatePassword through the host's ISecretResolver (optional ctor param, DI-injected). Fail-closed: a secret: ref with no store throws instead of being sent as the literal (the browse used to send a secret: Password verbatim). The identity is built before any network I/O. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+56
-2
@@ -54,10 +54,64 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken)
|
||||
// R06:O-5 — reads the node's NodeClass / DisplayName / DataType / ValueRank / UserAccessLevel in one request and
|
||||
// maps them through the same UpstreamAttributeMapping the runtime driver's discovery registers with, so the
|
||||
// side-panel shows what a browse-commit would author. An Object (folder) has no value attributes → empty.
|
||||
// Throws ArgumentException when nodeId cannot be resolved against the live session's namespace table.
|
||||
public async Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!NamespaceMap.TryResolve(_session, nodeId, out var resolved))
|
||||
throw new ArgumentException(
|
||||
$"Cannot resolve NodeId '{nodeId}' against the live session.", nameof(nodeId));
|
||||
|
||||
uint[] attributeIds =
|
||||
[
|
||||
Attributes.NodeClass, Attributes.DisplayName, Attributes.DataType, Attributes.ValueRank,
|
||||
Attributes.UserAccessLevel,
|
||||
];
|
||||
var toRead = new ReadValueIdCollection(attributeIds.Length);
|
||||
foreach (var id in attributeIds) toRead.Add(new ReadValueId { NodeId = resolved, AttributeId = id });
|
||||
|
||||
DataValueCollection values;
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var resp = await _session.ReadAsync(
|
||||
requestHeader: null,
|
||||
maxAge: 0,
|
||||
timestampsToReturn: TimestampsToReturn.Neither,
|
||||
nodesToRead: toRead,
|
||||
ct: cancellationToken).ConfigureAwait(false);
|
||||
values = resp?.Results ?? [];
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
LastUsedUtc = DateTime.UtcNow;
|
||||
return Task.FromResult<IReadOnlyList<AttributeInfo>>(Array.Empty<AttributeInfo>());
|
||||
|
||||
DataValue? At(int i) => i < values.Count && StatusCode.IsGood(values[i].StatusCode)
|
||||
? values[i] : null;
|
||||
|
||||
var nodeClass = At(0)?.Value is int nc ? (NodeClass)nc : NodeClass.Unspecified;
|
||||
if (nodeClass != NodeClass.Variable) return Array.Empty<AttributeInfo>();
|
||||
|
||||
var name = (At(1)?.Value as LocalizedText)?.Text ?? resolved.ToString() ?? nodeId;
|
||||
var dataType = At(2)?.Value is NodeId dt
|
||||
? UpstreamAttributeMapping.MapDataType(dt)
|
||||
: ZB.MOM.WW.OtOpcUa.Core.Abstractions.DriverDataType.String;
|
||||
var valueRank = At(3)?.Value is int vr ? vr : ValueRanks.Scalar;
|
||||
var access = At(4)?.Value is byte ab ? ab : (byte)0;
|
||||
|
||||
return
|
||||
[
|
||||
new AttributeInfo(
|
||||
Name: name,
|
||||
DriverDataType: dataType.ToString(),
|
||||
IsArray: UpstreamAttributeMapping.IsArray(valueRank),
|
||||
SecurityClass: UpstreamAttributeMapping.MapAccessLevel(access).ToString()),
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>Issue a single-level Browse (plus continuation-point follow-ups) under the
|
||||
|
||||
Reference in New Issue
Block a user