using System.CommandLine; using System.CommandLine.Parsing; using System.Text.Json; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; namespace ZB.MOM.WW.ScadaBridge.CLI.Commands; /// /// Builds the two-person secured-write workflow. Submission requires an Operator; /// approve/reject require a distinct Verifier. Roles, site scope, expiry, and /// separation of duties are enforced by the management server. /// public static class SecuredWriteCommands { // Options are static so the parsed values can be read back from both the SetAction // and the internal BuildListCommand helper (used by command-construction tests). private static readonly Option StatusOption = new("--status") { Description = "Filter by status (e.g. Pending, Approved, Rejected, Expired, Executed, Failed); omit to match all" }; private static readonly Option SiteOption = new("--site") { Description = "Filter by site identifier; omit to match all sites" }; private static readonly Option SkipOption = new("--skip") { Description = "Number of rows to skip (offset paging)", DefaultValueFactory = _ => 0 }; private static readonly Option TakeOption = new("--take") { Description = "Maximum rows to return (clamped to 1..500 server-side)", DefaultValueFactory = _ => 200 }; /// Builds the secured-write command group with list, submit, approve, and reject subcommands. /// Global --url option for the management API endpoint. /// Global --format option for output format. /// Global --username option for authentication. /// Global --password option for authentication. /// The configured secured-write command. public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var command = new Command("secured-write") { Description = "Manage two-person (MxGateway) secured writes" }; command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildSubmit(urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildDecision("approve", urlOption, formatOption, usernameOption, passwordOption)); command.Add(BuildDecision("reject", urlOption, formatOption, usernameOption, passwordOption)); return command; } private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var cmd = new Command("list") { Description = "List secured writes (pending queue + terminal history), optionally filtered and paged" }; cmd.Add(StatusOption); cmd.Add(SiteOption); cmd.Add(SkipOption); cmd.Add(TakeOption); cmd.SetAction(async (ParseResult result) => { return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, BuildListCommand(result)); }); return cmd; } private static Command BuildSubmit(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var site = new Option("--site") { Description = "Target site identifier", Required = true }; var connection = new Option("--connection") { Description = "MxGateway connection name", Required = true }; var tag = new Option("--tag-path") { Description = "Fully-qualified tag path", Required = true }; var value = new Option("--value-json") { Description = "JSON-serialized value (quote JSON strings)", Required = true }; var type = new Option("--value-type") { Description = "Target data type name, e.g. Boolean or Double", Required = true }; var comment = new Option("--comment") { Description = "Operator comment" }; var command = new Command("submit") { Description = "Submit a pending secured write as an Operator for a distinct Verifier" }; command.Add(site); command.Add(connection); command.Add(tag); command.Add(value); command.Add(type); command.Add(comment); command.SetAction(async (ParseResult result) => { var json = result.GetValue(value)!; try { using var parsed = JsonDocument.Parse(json); } catch (JsonException ex) { OutputFormatter.WriteError($"Invalid value JSON: {ex.Message}", "INVALID_JSON"); return 1; } return await CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption, new SubmitSecuredWriteCommand(result.GetValue(site)!, result.GetValue(connection)!, result.GetValue(tag)!, json, result.GetValue(type)!, result.GetValue(comment))); }); return command; } private static Command BuildDecision(string verb, Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var id = new Option("--id") { Description = "Pending secured-write ID", Required = true }; var comment = new Option("--comment") { Description = "Verifier comment" }; var command = new Command(verb) { Description = verb == "approve" ? "Approve and execute a pending write as a distinct Verifier" : "Reject a pending write as a distinct Verifier" }; command.Add(id); command.Add(comment); command.SetAction((ParseResult result) => { object request = verb == "approve" ? new ApproveSecuredWriteCommand(result.GetValue(id), result.GetValue(comment)) : new RejectSecuredWriteCommand(result.GetValue(id), result.GetValue(comment)); return CommandHelpers.ExecuteCommandAsync(result, urlOption, formatOption, usernameOption, passwordOption, request, onSuccess: verb == "approve" ? json => HandleApprovalResponse(json, CommandHelpers.ResolveFormat(result, formatOption, CliConfig.Load())) : null); }); return command; } private static int HandleApprovalResponse(string json, string format) { CommandHelpers.HandleResponse(new ManagementResponse(200, json, null, null), format); try { using var document = JsonDocument.Parse(json); var root = document.RootElement; if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("status", out var status) && status.ValueKind == JsonValueKind.String) { if (status.GetString() == "Executed") return 0; if (status.GetString() == "Failed") { var detail = root.TryGetProperty("executionError", out var error) && error.ValueKind == JsonValueKind.String ? error.GetString() : null; OutputFormatter.WriteError(detail ?? "The server reported that the secured write failed.", "SECURED_WRITE_FAILED"); return 1; } } } catch (JsonException) { } OutputFormatter.WriteError("The server returned an invalid secured-write approval outcome.", "INVALID_RESPONSE"); return 1; } /// /// Builds a from a parsed secured-write list /// invocation. Exposed internally so command-construction tests can assert field mapping /// (including the --skip / --take paging options) without triggering the HTTP call. /// /// The parsed command-line result from the secured-write list invocation. /// A populated from the parsed result. internal static ListSecuredWritesCommand BuildListCommand(ParseResult result) { var status = result.GetValue(StatusOption); var site = result.GetValue(SiteOption); var skip = result.GetValue(SkipOption); var take = result.GetValue(TakeOption); return new ListSecuredWritesCommand(status, site, skip, take); } }