b2cab59cb9
The command passed no `tableProjector`, so `--format table` handed the renderer
an `AuditLogQueryResult` OBJECT. That takes the generic Property/Value branch,
which printed the entire `entries` array as raw JSON inside a single cell. JSON
output was and remains correct, and is the primary format for this command.
Adds `AuditConfigTableProjection`, mirroring `TemplateTableProjection`:
string->string over the raw body, applied only on the table path, returning the
input verbatim when it is not JSON or not a recognised shape so the renderer's
own fallbacks still apply. It unwraps the envelope to a row array — id,
timestamp, user, action, entityType, entityName, entityId — and drops
`afterStateJson`, the full post-change entity snapshot, which is the audit
analogue of template's fat child arrays: one per row fills a terminal.
`bundleImportId` is dropped as mostly-null noise.
`totalCount` would be lost by a plain unwrap, so a truncated page gains a single
trailing row with a `note` cell ("showing 50 of 137 — use --page / --page-size
for the rest"). When the page IS the whole result set the column is not emitted
at all, so a small query's table stays exactly as narrow as its data, and the
note row carries no `id` so it cannot be mistaken for an entry.
A `--detail` flag skips the projection and renders the raw payload, matching
`template list` / `template get` — that plus `--format json` is how to reach
`afterStateJson`. A bare array root is also accepted, so the projection does not
regress if the payload is ever unwrapped upstream or the deprecated `audit-log`
alias meets an older server.
Tests build their fixtures by serialising the REAL AuditLogQueryResult /
AuditLogEntry types, so renaming a server property fails the test instead of
silently blanking a column. CLI README + Component-CLI.md document the table
shape, the omitted fields, and --detail.
113 lines
5.9 KiB
C#
113 lines
5.9 KiB
C#
using System.CommandLine;
|
|
using System.CommandLine.Parsing;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
/// <summary>
|
|
/// The <c>scadabridge audit-config</c> command group: views the configuration-change
|
|
/// audit log (the <c>IAuditService</c> trail of admin edits — distinct from the
|
|
/// centralized append-only Audit Log served by <see cref="AuditCommands"/>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Renamed from <c>audit-log</c> to avoid confusion with the new
|
|
/// <c>scadabridge audit</c> group. The old <c>audit-log</c> name is retained as a
|
|
/// deprecated alias; <see cref="DeprecatedAlias"/> still resolves the full subcommand
|
|
/// tree, and <c>Program.cs</c> prints a deprecation warning when it is used.
|
|
/// </remarks>
|
|
public static class AuditLogCommands
|
|
{
|
|
/// <summary>The deprecated alias kept for backward compatibility with the old command name.</summary>
|
|
public const string DeprecatedAlias = "audit-log";
|
|
|
|
/// <summary>The deprecation warning emitted when the old <c>audit-log</c> name is used.</summary>
|
|
public const string DeprecationWarning =
|
|
"Warning: 'audit-log' is deprecated and will be removed in a future release. "
|
|
+ "Use 'audit-config' instead.";
|
|
|
|
/// <summary>
|
|
/// Writes the <see cref="DeprecationWarning"/> to <paramref name="stderr"/> when the
|
|
/// CLI was invoked via the deprecated <c>audit-log</c> command name (i.e. the first
|
|
/// argument is <see cref="DeprecatedAlias"/>). The command itself still works — it is
|
|
/// an alias of <c>audit-config</c> — so this only adds the migration warning.
|
|
/// Factored out of <c>Program.cs</c> so it is unit-testable without spawning a process.
|
|
/// </summary>
|
|
/// <param name="args">The raw command-line arguments passed to the CLI.</param>
|
|
/// <param name="stderr">The text writer to emit the deprecation warning to.</param>
|
|
public static void WriteDeprecationWarningIfNeeded(string[] args, TextWriter stderr)
|
|
{
|
|
if (args.Length > 0
|
|
&& string.Equals(args[0], DeprecatedAlias, StringComparison.Ordinal))
|
|
{
|
|
stderr.WriteLine(DeprecationWarning);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the <c>audit-config</c> command (with the deprecated <c>audit-log</c> alias) and its subcommands.
|
|
/// </summary>
|
|
/// <param name="urlOption">Global management URL option.</param>
|
|
/// <param name="formatOption">Global output format option.</param>
|
|
/// <param name="usernameOption">Global username option.</param>
|
|
/// <param name="passwordOption">Global password option.</param>
|
|
/// <returns>The configured <c>audit-config</c> command with all sub-commands registered.</returns>
|
|
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var command = new Command("audit-config") { Description = "Query the configuration-change audit log" };
|
|
// Backward-compatible alias for the old `audit-log` name. The alias keeps
|
|
// full subcommand parity automatically; the deprecation warning is emitted by
|
|
// the args[0] check in Program.cs.
|
|
command.Aliases.Add(DeprecatedAlias);
|
|
|
|
command.Add(BuildQuery(urlOption, formatOption, usernameOption, passwordOption));
|
|
|
|
return command;
|
|
}
|
|
|
|
private static Command BuildQuery(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var userOption = new Option<string?>("--user") { Description = "Filter by username" };
|
|
var entityTypeOption = new Option<string?>("--entity-type") { Description = "Filter by entity type" };
|
|
var actionOption = new Option<string?>("--action") { Description = "Filter by action" };
|
|
var fromOption = new Option<DateTimeOffset?>("--from") { Description = "Start date (ISO 8601)" };
|
|
var toOption = new Option<DateTimeOffset?>("--to") { Description = "End date (ISO 8601)" };
|
|
var pageOption = new Option<int>("--page") { Description = "Page number" };
|
|
pageOption.DefaultValueFactory = _ => 1;
|
|
var pageSizeOption = new Option<int>("--page-size") { Description = "Page size" };
|
|
pageSizeOption.DefaultValueFactory = _ => 50;
|
|
var detailOption = new Option<bool>("--detail")
|
|
{
|
|
Description = "Render the raw query payload in table output instead of the compact column projection. "
|
|
+ "This is how to see 'afterStateJson' (the full post-change entity snapshot) in table form — it is "
|
|
+ "omitted from the compact columns because a single entry's snapshot fills the terminal. "
|
|
+ "No effect on JSON output."
|
|
};
|
|
|
|
var cmd = new Command("query") { Description = "Query audit log entries" };
|
|
cmd.Add(userOption);
|
|
cmd.Add(entityTypeOption);
|
|
cmd.Add(actionOption);
|
|
cmd.Add(fromOption);
|
|
cmd.Add(toOption);
|
|
cmd.Add(pageOption);
|
|
cmd.Add(pageSizeOption);
|
|
cmd.Add(detailOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var user = result.GetValue(userOption);
|
|
var entityType = result.GetValue(entityTypeOption);
|
|
var action = result.GetValue(actionOption);
|
|
var from = result.GetValue(fromOption);
|
|
var to = result.GetValue(toOption);
|
|
var page = result.GetValue(pageOption);
|
|
var pageSize = result.GetValue(pageSizeOption);
|
|
var detail = result.GetValue(detailOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
new QueryAuditLogCommand(user, entityType, action, from, to, page, pageSize),
|
|
tableProjector: detail ? null : AuditConfigTableProjection.ProjectEntries);
|
|
});
|
|
return cmd;
|
|
}
|
|
}
|