Implements configurable user authentication (anonymous + username/password) with pluggable credential provider (IUserAuthenticationProvider). Anonymous writes can be disabled via AnonymousCanWrite setting while reads remain open. Adds -U/-P flags to all CLI commands for authenticated sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6.3 KiB
Read/Write Operations
LmxNodeManager overrides the OPC UA Read and Write methods to translate client requests into MXAccess runtime calls. Each override resolves the OPC UA NodeId to a Galaxy tag reference, performs the I/O through IMxAccessClient, and returns the result with appropriate status codes.
Read Override
The Read override in LmxNodeManager intercepts value attribute reads for nodes in the Galaxy namespace.
Resolution flow
- The base class
Readruns first, handling non-value attributes (DisplayName, DataType, etc.) through the standard node manager. - For each
ReadValueIdwhereAttributeId == Attributes.Value, the override checks whether the node belongs to this namespace (NamespaceIndexmatch). - The string-typed
NodeId.Identifieris looked up in_nodeIdToTagReferenceto find the correspondingFullTagReference(e.g.,DelmiaReceiver_001.DownloadPath). _mxAccessClient.ReadAsync(tagRef)retrieves the current value, timestamp, and quality from MXAccess. The async call is synchronously awaited because the OPC UA SDKReadoverride is synchronous.- The returned
Vtqis converted to aDataValueviaCreatePublishedDataValue, which normalizes array values throughNormalizePublishedValue(substituting a default typed array when the value is null for array nodes). - On success,
errors[i]is set toServiceResult.Good. On exception, the error is set toBadInternalError.
if (_nodeIdToTagReference.TryGetValue(nodeIdStr, out var tagRef))
{
var vtq = _mxAccessClient.ReadAsync(tagRef).GetAwaiter().GetResult();
results[i] = CreatePublishedDataValue(tagRef, vtq);
errors[i] = ServiceResult.Good;
}
Write Override
The Write override follows a similar pattern but includes access-level enforcement and array element write support.
Access level check
The base class Write runs first and sets BadNotWritable for nodes whose AccessLevel does not include CurrentWrite. The override skips these nodes:
if (errors[i] != null && errors[i].StatusCode == StatusCodes.BadNotWritable)
continue;
The AccessLevel is set during node creation based on SecurityClassificationMapper.IsWritable(attr.SecurityClassification). Read-only Galaxy attributes (e.g., security classification FreeRead) get AccessLevels.CurrentRead only.
Write flow
- The
NodeIdis resolved to a tag reference via_nodeIdToTagReference. - The raw value is extracted from
writeValue.Value.WrappedValue.Value. - If the write includes an
IndexRange(array element write),TryApplyArrayElementWritehandles the merge before sending the full array to MXAccess. _mxAccessClient.WriteAsync(tagRef, value)sends the value to the Galaxy runtime.- On success,
PublishLocalWriteupdates the in-memory node immediately so subscribed clients see the change without waiting for the next MXAccess data change callback.
Array element writes via IndexRange
TryApplyArrayElementWrite supports writing individual elements of an array attribute. MXAccess does not support element-level writes, so the method performs a read-modify-write:
- Parse the
IndexRangestring as a zero-based integer index. ReturnBadIndexRangeInvalidif parsing fails or the index is negative. - Read the current array value from MXAccess via
ReadAsync. - Clone the array and set the element at the target index.
NormalizeIndexedWriteValueunwraps single-element arrays (OPC UA clients sometimes wrap a scalar in a one-element array).ConvertArrayElementValuecoerces the value to the array's element type usingConvert.ChangeType, handling null values by substituting the type's default.- The full modified array is written back to MXAccess as a single
WriteAsynccall.
var nextArray = (Array)currentArray.Clone();
nextArray.SetValue(ConvertArrayElementValue(normalizedValue, elementType), index);
updatedArray = nextArray;
Role-based write enforcement
When AnonymousCanWrite is false in the Authentication configuration, the write override enforces role-based access control before dispatching to MXAccess. The check order is:
- The base class
Writeruns first, enforcingAccessLevel. Nodes withoutCurrentWritegetBadNotWritableand the override skips them. - The override checks whether the node is in the Galaxy namespace. Non-namespace nodes are skipped.
- If
AnonymousCanWriteisfalse, the override inspectscontext.OperationContext.SessionforGrantedRoleIds. If the session does not holdWellKnownRole_AuthenticatedUser, the error is set toBadUserAccessDeniedand the write is rejected. - If the role check passes (or
AnonymousCanWriteistrue), the write proceeds to MXAccess.
The existing security classification enforcement (ReadOnly nodes getting BadNotWritable via AccessLevel) still applies first and takes precedence over the role check.
Value Type Conversion
CreatePublishedDataValue wraps the conversion pipeline. NormalizePublishedValue checks whether the tag is an array type with a declared ArrayDimension and substitutes a default typed array (via CreateDefaultArrayValue) when the raw value is null. This prevents OPC UA clients from receiving a null variant for array nodes, which violates the specification for nodes declared with ValueRank.OneDimension.
CreateDefaultArrayValue uses MxDataTypeMapper.MapToClrType to determine the CLR element type, then creates an Array.CreateInstance of the declared length. String arrays are initialized with string.Empty elements rather than null.
PublishLocalWrite
After a successful write, PublishLocalWrite updates the variable node in memory without waiting for the MXAccess OnDataChange callback to arrive:
private void PublishLocalWrite(string tagRef, object? value)
{
var dataValue = CreatePublishedDataValue(tagRef, Vtq.Good(value));
variable.Value = dataValue.Value;
variable.StatusCode = dataValue.StatusCode;
variable.Timestamp = dataValue.SourceTimestamp;
variable.ClearChangeMasks(SystemContext, false);
}
ClearChangeMasks notifies the OPC UA framework that the node value has changed, which triggers data change notifications to any active monitored items. Without this call, subscribed clients would only see the update when the next MXAccess data change event arrives, which could be delayed depending on the subscription interval.