fix(templateengine): audit projected template/script summaries instead of the tracked graph and script bodies (review 2026-10-21 08/G1)

This commit is contained in:
Joseph Doherty
2026-10-22 06:28:17 -04:00
parent c0792cff3e
commit 52aab37299
5 changed files with 232 additions and 7 deletions
@@ -78,7 +78,7 @@ public class SharedScriptService
// SaveChangesAsync ran.
await _repository.AddSharedScriptAsync(script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(user, "Create", "SharedScript", script.Id.ToString(), name, script, cancellationToken);
await _auditService.LogAsync(user, "Create", "SharedScript", script.Id.ToString(), name, TemplateAuditShapes.SharedScript(script), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result<SharedScript>.Success(script);
@@ -123,7 +123,7 @@ public class SharedScriptService
script.ReturnDefinition = returnDefinition;
await _repository.UpdateSharedScriptAsync(script, cancellationToken);
await _auditService.LogAsync(user, "Update", "SharedScript", scriptId.ToString(), script.Name, script, cancellationToken);
await _auditService.LogAsync(user, "Update", "SharedScript", scriptId.ToString(), script.Name, TemplateAuditShapes.SharedScript(script), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result<SharedScript>.Success(script);
@@ -0,0 +1,78 @@
using System.Security.Cryptography;
using System.Text;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;
/// <summary>
/// Compact after-state shapes for the configuration audit log (review 2026-10-21
/// 08/G1). <c>IAuditService.LogAsync</c> serialises whatever it is handed into
/// <c>AuditLogEntries.AfterStateJson</c> — an indefinitely-retained table — so
/// handing it a tracked <see cref="Template"/> wrote the whole loaded graph,
/// every attribute, alarm and script body included, on each create / rename / move.
/// These shapes keep the identity, the scalar fields, per-collection member counts
/// and the fields that changed. Script bodies are never written: a body is
/// represented by its length and SHA-256, which still lets an auditor tell whether
/// (and to what) a body changed without storing it again on every edit.
/// </summary>
internal static class TemplateAuditShapes
{
/// <summary>Summary of a template: identity, scalars and member counts — no member rows.</summary>
/// <param name="template">The template.</param>
/// <param name="changedFields">Names of the fields this operation changed, if known.</param>
/// <returns>The audit after-state object.</returns>
internal static object Template(Template template, IReadOnlyList<string>? changedFields = null) => new
{
template.Id,
template.Name,
template.Description,
template.ParentTemplateId,
template.FolderId,
template.IsDerived,
template.OwnerCompositionId,
AttributeCount = template.Attributes.Count,
AlarmCount = template.Alarms.Count,
ScriptCount = template.Scripts.Count,
CompositionCount = template.Compositions.Count,
NativeAlarmSourceCount = template.NativeAlarmSources.Count,
ChangedFields = changedFields,
};
/// <summary>A template script without its body (length + SHA-256 instead).</summary>
/// <param name="script">The script.</param>
/// <returns>The audit after-state object.</returns>
internal static object Script(TemplateScript script) => new
{
script.Id,
script.TemplateId,
script.Name,
script.IsLocked,
script.LockedInDerived,
script.IsInherited,
script.TriggerType,
script.TriggerConfiguration,
script.ParameterDefinitions,
script.ReturnDefinition,
script.MinTimeBetweenRuns,
script.ExecutionTimeoutSeconds,
CodeLength = script.Code?.Length ?? 0,
CodeSha256 = HashCode(script.Code),
};
/// <summary>A shared script without its body (length + SHA-256 instead).</summary>
/// <param name="script">The shared script.</param>
/// <returns>The audit after-state object.</returns>
internal static object SharedScript(SharedScript script) => new
{
script.Id,
script.Name,
script.ParameterDefinitions,
script.ReturnDefinition,
CodeLength = script.Code?.Length ?? 0,
CodeSha256 = HashCode(script.Code),
};
private static string? HashCode(string? code) =>
code == null ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(code)));
}
@@ -84,7 +84,7 @@ public class TemplateService
// to the row it described.
await _repository.AddTemplateAsync(template, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(user, "Create", "Template", template.Id.ToString(), name, template, cancellationToken);
await _auditService.LogAsync(user, "Create", "Template", template.Id.ToString(), name, TemplateAuditShapes.Template(template), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result<Template>.Success(template);
@@ -130,6 +130,12 @@ public class TemplateService
"Parent template cannot be changed after creation.");
}
var changedFields = new List<string>();
if (!string.Equals(template.Name, name, StringComparison.Ordinal))
changedFields.Add(nameof(Template.Name));
if (description != null && !string.Equals(template.Description, description, StringComparison.Ordinal))
changedFields.Add(nameof(Template.Description));
template.Name = name;
// Leave-unchanged semantics: only overwrite the description when a value is
// supplied. null = not provided (keep existing); "" = explicit clear.
@@ -142,7 +148,10 @@ public class TemplateService
return Result<Template>.Failure(collisionResult);
await _repository.UpdateTemplateAsync(template, cancellationToken);
await _auditService.LogAsync(user, "Update", "Template", templateId.ToString(), name, template, cancellationToken);
// Audit a projected summary, never the tracked graph with every script body
// (review 2026-10-21 08/G1).
await _auditService.LogAsync(user, "Update", "Template", templateId.ToString(), name,
TemplateAuditShapes.Template(template, changedFields), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result<Template>.Success(template);
@@ -234,7 +243,8 @@ public class TemplateService
template.FolderId = newFolderId;
await _repository.UpdateTemplateAsync(template, cancellationToken);
await _auditService.LogAsync(user, "Move", "Template", template.Id.ToString(), template.Name, template, cancellationToken);
await _auditService.LogAsync(user, "Move", "Template", template.Id.ToString(), template.Name,
TemplateAuditShapes.Template(template, [nameof(Template.FolderId)]), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
return Result<Template>.Success(template);
@@ -723,7 +733,7 @@ public class TemplateService
// real script Id rather than a literal "0".
await _repository.AddTemplateScriptAsync(script, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(user, "Create", "TemplateScript", script.Id.ToString(), script.Name, script, cancellationToken);
await _auditService.LogAsync(user, "Create", "TemplateScript", script.Id.ToString(), script.Name, TemplateAuditShapes.Script(script), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
// Propagate the new member to derived descendants.
@@ -809,7 +819,7 @@ public class TemplateService
// Name is NOT updated (fixed)
await _repository.UpdateTemplateScriptAsync(existing, cancellationToken);
await _auditService.LogAsync(user, "Update", "TemplateScript", scriptId.ToString(), existing.Name, existing, cancellationToken);
await _auditService.LogAsync(user, "Update", "TemplateScript", scriptId.ToString(), existing.Name, TemplateAuditShapes.Script(existing), cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
// Propagate the changed effective value to derived descendants.