fix(heidenhain): refuse literal credentials and writable/mistyped tags at the save path (#803 review)

- Review 1: RawTreeService Create/UpdateDriver and Create/UpdateDevice refuse a Heidenhain
  config whose password fields are not blank / secret:heidenhain/... / env:OTOPCUA_HEIDENHAIN_...
  (DriverConfigSaveGate over HeidenhainOptionsParser.DescribeCredentialProblems: top level,
  Access, Devices[], case-insensitive keys; the value is never echoed), so a literal password
  never reaches the draft DB. The driver and device modals show the reason and disable Save.
- Review 8: CreateTag/UpdateTag/ImportTags (tag modal, manual entry, CSV import, browse commit)
  refuse a ReadWrite Heidenhain tag or a Tag.DataType that disagrees with what the driver
  publishes (HeidenhainTagRules: catalog type, Float64 => Double, or the binding's dataType).
  The tag modal and manual-entry grid fix the access level at Read and derive the data type from
  the address (RawTagRowDefaults). New deploy gate HeidenhainTagDataTypeMismatch.
- docs/drivers/Heidenhain.md: credential namespace convention + why, the save-path and deploy
  enforcement, optional-role credential behaviour, BadConfigurationError, backoff/ordering notes.

Tests: HeidenhainSavePathTests (service save path; red with the gates disabled) and new
DraftValidatorHeidenhainTests cases.
This commit is contained in:
Joseph Doherty
2026-11-10 23:54:32 -05:00
parent f7a3588b24
commit 54f58c583c
15 changed files with 427 additions and 33 deletions
@@ -0,0 +1,54 @@
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Heidenhain;
/// <summary>
/// Rules about the Tag ROW (not its TagConfig) for a Heidenhain tag: the one authority shared by every AdminUI
/// authoring path (RawTagModal, manual entry, CSV import, browse commit — all through <c>RawTreeService</c>)
/// and the deploy gate, so authoring and deploy can never disagree.
/// </summary>
public static class HeidenhainTagRules
{
/// <summary>
/// The <c>Tag.DataType</c> (OPC UA built-in type name) a Heidenhain tag must carry for the value the driver
/// publishes, or null when the TagConfig does not resolve (the TagConfig gate reports that).
/// </summary>
/// <param name="tagConfig">The TagConfig JSON.</param>
/// <returns>e.g. <c>Int32</c>, <c>Double</c>, <c>String</c>; null when unresolvable.</returns>
public static string? ExpectedTagDataType(string? tagConfig) =>
HeidenhainTagDefinitionFactory.TryFromTagConfig("rule", "", tagConfig, out var def, out _)
? OpcTypeName(def.DataType)
: null;
/// <summary>The OPC UA built-in type name (the <c>Tag.DataType</c> vocabulary) for a published data type.</summary>
/// <param name="type">The driver data type.</param>
/// <returns>The type name.</returns>
public static string OpcTypeName(DriverDataType type) => type switch
{
DriverDataType.Boolean => "Boolean",
DriverDataType.Int16 => "Int16",
DriverDataType.Int32 => "Int32",
DriverDataType.Int64 => "Int64",
DriverDataType.Float64 => "Double",
DriverDataType.String => "String",
_ => type.ToString(),
};
/// <summary>
/// Why a Heidenhain tag row may not be saved / deployed: a writable access level (the driver is read-only),
/// or a <c>Tag.DataType</c> that disagrees with what the driver publishes for its address.
/// </summary>
/// <param name="readWrite">Whether the row's access level is ReadWrite.</param>
/// <param name="tagDataType">The row's <c>Tag.DataType</c>.</param>
/// <param name="tagConfig">The row's TagConfig.</param>
/// <returns>The reason, or null when the row is acceptable.</returns>
public static string? DescribeRowProblem(bool readWrite, string? tagDataType, string? tagConfig)
{
if (readWrite)
return "Heidenhain tags are read-only — set the access level to Read (the driver never writes to the control).";
var expected = ExpectedTagDataType(tagConfig);
if (expected is not null && !string.Equals(expected, tagDataType, StringComparison.OrdinalIgnoreCase))
return $"Data type {tagDataType} does not match what the driver publishes for this address ({expected}).";
return null;
}
}