fix(auth): C4 review polish — document backward-compat JSON tolerance, shared BundleJsonOptions, PreviewAsync legacy-bundle test, doc fix (review I-2/I-3/M-1/M-2; I-1 intentionally skipped)

This commit is contained in:
Joseph Doherty
2026-06-02 05:15:50 -04:00
parent 731cfd3bfc
commit b13d7b3d28
5 changed files with 134 additions and 15 deletions
@@ -1,7 +1,6 @@
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
@@ -45,12 +44,14 @@ namespace ZB.MOM.WW.ScadaBridge.Transport.Import;
/// </summary>
public sealed class BundleImporter : IBundleImporter
{
private static readonly JsonSerializerOptions ContentJsonOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
// All bundle content deserialization goes through BundleJsonOptions.Default.
// IMPORTANT — unknown JSON properties must remain ALLOWED (the default
// JsonUnmappedMemberHandling.Skip). Pre-C4 bundles may carry a top-level
// "apiKeys" array and/or "ApprovedApiKeyIds" inside "apiMethods[]" entries.
// Setting JsonUnmappedMemberHandling.Disallow here would cause those bundles
// to fail deserialization, breaking backward-compat. This tolerance is
// load-bearing and must not be changed. (Fix I-2)
private static readonly JsonSerializerOptions ContentJsonOptions = BundleJsonOptions.Default;
private readonly BundleSerializer _bundleSerializer;
private readonly ManifestValidator _manifestValidator;
@@ -0,0 +1,37 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;
/// <summary>
/// Canonical <see cref="JsonSerializerOptions"/> shared by every component that
/// reads or writes a bundle content payload.
/// <para>
/// <b>Important — unknown-property tolerance:</b>
/// <see cref="JsonUnmappedMemberHandling"/> is left at its default
/// (<c>Skip</c>); setting it to <c>Disallow</c> would break backward-
/// compatible deserialization of pre-C4 bundles, which may carry a top-level
/// <c>apiKeys</c> array and/or <c>ApprovedApiKeyIds</c> fields inside
/// <c>apiMethods[]</c> entries. That tolerance is load-bearing and must be
/// preserved.
/// </para>
/// <para>
/// <b><c>WhenWritingNull</c>:</b> new bundles never emit a top-level
/// <c>ApiKeys</c> property (it defaults to <c>null</c> on
/// <see cref="BundleContentDto"/>); <c>WhenWritingNull</c> ensures the field
/// is omitted entirely from the JSON so the wire format stays clean.
/// </para>
/// </summary>
internal static class BundleJsonOptions
{
/// <summary>
/// The single shared <see cref="JsonSerializerOptions"/> instance for
/// all bundle content serialization and deserialization.
/// </summary>
internal static readonly JsonSerializerOptions Default = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
}
@@ -1,6 +1,5 @@
using System.IO.Compression;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
using ZB.MOM.WW.ScadaBridge.Transport.Encryption;
@@ -26,12 +25,9 @@ public sealed class BundleSerializer
private const string ContentJsonEntryName = "content.json";
private const string ContentEncEntryName = "content.enc";
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
// All bundle content serialization goes through BundleJsonOptions.Default —
// see that class for the rationale (WhenWritingNull + unknown-member tolerance).
private static readonly JsonSerializerOptions JsonOptions = BundleJsonOptions.Default;
/// <summary>
/// Serializes the bundle content to its canonical JSON byte form. Exposed
@@ -30,7 +30,8 @@ public sealed record EntityAggregate(
/// Top-level serializable bundle payload. Lists are sequenced in dependency
/// order so importers can apply them inline. Lists are never null on the wire
/// — empty arrays are preferred over nulls so JSON consumers can rely on each
/// property being present.
/// property being present — except <see cref="ApiKeys"/>, which is intentionally
/// null-defaulted (see below).
/// <para>
/// <see cref="ApiKeys"/> is a <b>legacy, read-only</b> field retained purely for
/// backward-compatible deserialization of bundles produced before the