diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index 8f82018b..85a7529d 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -332,6 +332,26 @@ The same validation logic is available to Design users in the Central UI without Gating against the instance surface is a design decision worth knowing: a shared script executes inline inside the calling script's context, so it is type-checked as if it were an instance script. A shared script that references an identifier the instance surface does not expose fails at save, and one that uses an instance accessor (`Attributes`, `Instance`, …) compiles — whether that accessor is meaningful at run time depends on the caller. +## Configuration Audit Shape + +`IAuditService.LogAsync` serialises the after-state it is handed into the +indefinitely-retained `AuditLogEntries.AfterStateJson`. Template Engine services +therefore hand it a **projected summary** for aggregates, never a tracked entity +graph (review 2026-10-21 08/G1, `TemplateAuditShapes`): + +- **Template** create / update / move — identity and scalar fields (`Id`, `Name`, + `Description`, `ParentTemplateId`, `FolderId`, `IsDerived`, `OwnerCompositionId`), + per-collection member counts, and `ChangedFields` (e.g. `Name`, `Description`, + `FolderId`). No member rows. A rename of a 40-script template used to write all 40 + script bodies. +- **Template script** and **shared script** create / update — every scalar field + except the body, which is recorded as `CodeLength` + `CodeSha256`, so an auditor can + tell whether (and to what) a body changed without the table storing every revision + of every body. + +Small leaf entities (attributes, alarms, compositions, folders, areas, instance +overrides) are still audited as-is. + ## Dependencies - **Configuration Database (MS SQL)**: Stores all templates, instances, areas, and their relationships. diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs index c8b842fa..5fa3c438 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/SharedScriptService.cs @@ -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.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.Success(script); diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateAuditShapes.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateAuditShapes.cs new file mode 100644 index 00000000..3cc63bed --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateAuditShapes.cs @@ -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; + +/// +/// Compact after-state shapes for the configuration audit log (review 2026-10-21 +/// 08/G1). IAuditService.LogAsync serialises whatever it is handed into +/// AuditLogEntries.AfterStateJson — an indefinitely-retained table — so +/// handing it a tracked 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. +/// +internal static class TemplateAuditShapes +{ + /// Summary of a template: identity, scalars and member counts — no member rows. + /// The template. + /// Names of the fields this operation changed, if known. + /// The audit after-state object. + internal static object Template(Template template, IReadOnlyList? 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, + }; + + /// A template script without its body (length + SHA-256 instead). + /// The script. + /// The audit after-state object. + 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), + }; + + /// A shared script without its body (length + SHA-256 instead). + /// The shared script. + /// The audit after-state object. + 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))); +} diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs index 4eae7d2b..6b74965d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/TemplateService.cs @@ -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