From c8f31bd653badf0b3bfff89a696865a50d1acff2 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 3 May 2026 23:48:17 -0400 Subject: [PATCH] mxaccesscli: add --secured / --verifier-* options for WriteSecured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteCommand grows three new options: --secured Route the write through LMXProxyServer.WriteSecured(currentUserId, verifierUserId, value) instead of plain Write(value, userId). Required for attributes classified as Secured Write or Verified Write, and useful for testing whether the audit subsystem propagates user identity when explicitly told the write is "secured". --verifier-username Galaxy / OS username of the verifier for a two-person Verified Write. Implies --secured. --verifier-domain Domain composed with --verifier-username as '\'. --verifier-password Verifier password. Redacted in the JSON query echo. When --secured is on without a verifier, the same auth_user_id is used for both currentUserId and verifierUserId (single-user Secured Write semantics). When a verifier is provided, the CLI authenticates both users and bails cleanly with "verifier-authentication-failed" on a verifier credential mismatch. The JSON envelope's results[] gains `secured` and `verifier_user_id` fields so an agent can confirm which path ran. MxItem grows WriteSecured(value, currentUserId, verifierUserId). Verified live against TestMachine_001.TestAlarm002.AckMsg under eOSUserBased + ArchestraUsers role: --secured succeeds with auth_user_id=1, verifier_user_id=1, MxCategoryOk. User_Name in the Historian Events row remains NULL — same as plain Write. The audit-attribution gate is not Write vs WriteSecured; running engines likely still need a redeploy to pick up the new security mode. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/MxAccess.Cli/Commands/WriteCommand.cs | 51 +++++++++++++++++-- mxaccesscli/src/MxAccess.Cli/Mx/MxItem.cs | 11 ++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/mxaccesscli/src/MxAccess.Cli/Commands/WriteCommand.cs b/mxaccesscli/src/MxAccess.Cli/Commands/WriteCommand.cs index 38a82f1..88f0b03 100644 --- a/mxaccesscli/src/MxAccess.Cli/Commands/WriteCommand.cs +++ b/mxaccesscli/src/MxAccess.Cli/Commands/WriteCommand.cs @@ -41,6 +41,18 @@ namespace MxAccess.Cli.Commands [CommandOption("client", Description = "MxAccess client name. Default 'mxa'.")] public string ClientName { get; init; } = "mxa"; + [CommandOption("secured", Description = "Route the write through LMXProxyServer.WriteSecured(currentUserId, verifierUserId, value) instead of Write(value, userId). Required for attributes whose security classification is Secured Write or Verified Write, and may be required for the engine audit trail to attribute the write to the authenticated user. Defaults to single-user Secured (currentUserId == verifierUserId == auth_user_id); see --verifier-username for two-person Verified Write.")] + public bool Secured { get; init; } + + [CommandOption("verifier-username", Description = "Galaxy / OS username of the verifier for a two-person Verified Write. Implies --secured. Combined with --verifier-domain as '\\' just like the operator credential set.")] + public string VerifierUsername { get; init; } + + [CommandOption("verifier-domain", Description = "Domain or hostname for the verifier OS user. Combined with --verifier-username.")] + public string VerifierDomain { get; init; } + + [CommandOption("verifier-password", Description = "Password for the verifier user. Redacted in the JSON query echo.")] + public string VerifierPassword { get; init; } + [CommandOption("llm-json", Description = "Emit the JSON envelope instead of human-readable status.")] public bool LlmJson { get; init; } @@ -78,6 +90,15 @@ namespace MxAccess.Cli.Commands ? Username : $@"{Domain}\{Username}"; } + string verifierVerifyUser = null; + if (!string.IsNullOrEmpty(VerifierUsername)) + { + verifierVerifyUser = string.IsNullOrEmpty(VerifierDomain) + ? VerifierUsername + : $@"{VerifierDomain}\{VerifierUsername}"; + } + // A verifier implies --secured, even if not explicitly set. + bool useSecured = Secured || verifierVerifyUser != null; var query = new { @@ -90,15 +111,19 @@ namespace MxAccess.Cli.Commands timeout_s = TimeoutSeconds, user_id = UserId, verify_user = verifyUser, + verifier_verify_user = verifierVerifyUser, + secured = useSecured, // Never echo the plaintext password — agents replay query // envelopes and would otherwise leak credentials. - password = string.IsNullOrEmpty(Password) ? null : "***", + password = string.IsNullOrEmpty(Password) ? null : "***", + verifier_password = string.IsNullOrEmpty(VerifierPassword) ? null : "***", client = ClientName, }; using var session = new MxSession(ClientName); MxItem item = null; - int effectiveUserId = UserId; + int effectiveUserId = UserId; + int verifierUserId = 0; try { // Resolve credentials -> userId before AddItem so we surface @@ -113,6 +138,21 @@ namespace MxAccess.Cli.Commands return default; } } + if (verifierVerifyUser != null) + { + verifierUserId = session.Authenticate(verifierVerifyUser, VerifierPassword ?? string.Empty); + if (verifierUserId == 0) + { + EmitFailure(console, query, "verifier-authentication-failed", Array.Empty()); + Environment.ExitCode = 1; + return default; + } + } + else if (useSecured) + { + // Single-user Secured Write — both ids are the operator. + verifierUserId = effectiveUserId; + } item = session.AddItem(Tag); @@ -153,7 +193,10 @@ namespace MxAccess.Cli.Commands return default; } - item.Write(coerced, effectiveUserId); + if (useSecured) + item.WriteSecured(coerced, effectiveUserId, verifierUserId); + else + item.Write(coerced, effectiveUserId); var got = session.WaitForUpdate( u => u.Kind == MxUpdateKind.WriteComplete && u.ItemHandle == item.Handle, @@ -179,6 +222,8 @@ namespace MxAccess.Cli.Commands error = ack.IsOk ? null : "write-failed", authenticated = verifyUser != null, auth_user_id = verifyUser != null ? (int?)effectiveUserId : null, + secured = useSecured, + verifier_user_id = useSecured ? (int?)verifierUserId : null, statuses = ack.Statuses, } }; diff --git a/mxaccesscli/src/MxAccess.Cli/Mx/MxItem.cs b/mxaccesscli/src/MxAccess.Cli/Mx/MxItem.cs index 214a598..413b95e 100644 --- a/mxaccesscli/src/MxAccess.Cli/Mx/MxItem.cs +++ b/mxaccesscli/src/MxAccess.Cli/Mx/MxItem.cs @@ -58,6 +58,17 @@ namespace MxAccess.Cli.Mx public void Write(object value, int userId = 0) => _proxy.Write(_hServer, Handle, value, userId); + /// Two-user Secured/Verified write. Propagates the user identity into + /// the alarm/event audit trail in a way that the engine's audit + /// subsystem honors for Secured Write / Verified Write attribute + /// security classifications. + /// + /// For single-user Secured Write, pass the same id for both + /// `currentUserId` and `verifierUserId`. For two-person Verified Write, + /// pass two distinct authenticated user ids (operator + verifier). + public void WriteSecured(object value, int currentUserId, int verifierUserId) => + _proxy.WriteSecured(_hServer, Handle, currentUserId, verifierUserId, value); + public void Dispose() { if (_disposed) return;