| | | 1 | | using System.Collections.ObjectModel; |
| | | 2 | | |
| | | 3 | | namespace AsiBackbone.Core.Metadata; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Defines optional host-owned budget limits for governance metadata dictionaries. |
| | | 7 | | /// </summary> |
| | | 8 | | /// <remarks> |
| | | 9 | | /// The budget is intentionally advisory and helper-oriented. AsiBackbone cannot determine whether |
| | | 10 | | /// host-provided metadata is sensitive in a specific deployment, but hosts can use these limits to |
| | | 11 | | /// keep audit and governance metadata bounded before durable storage, emission, or signing. |
| | | 12 | | /// </remarks> |
| | | 13 | | public sealed class GovernanceMetadataBudget |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Gets the default recommended maximum number of metadata entries. |
| | | 17 | | /// </summary> |
| | | 18 | | public const int DefaultMaxCount = 32; |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Gets the default recommended maximum metadata key length, in characters. |
| | | 22 | | /// </summary> |
| | | 23 | | public const int DefaultMaxKeyLength = 64; |
| | | 24 | | |
| | | 25 | | /// <summary> |
| | | 26 | | /// Gets the default recommended maximum metadata value length, in characters. |
| | | 27 | | /// </summary> |
| | | 28 | | public const int DefaultMaxValueLength = 512; |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Gets the default recommended estimated serialized metadata size, in UTF-8 bytes. |
| | | 32 | | /// </summary> |
| | | 33 | | public const int DefaultMaxSerializedBytes = 8192; |
| | | 34 | | |
| | 3 | 35 | | private static readonly ReadOnlyCollection<string> DefaultReservedKeyFragments = |
| | 3 | 36 | | new( |
| | 3 | 37 | | new[] |
| | 3 | 38 | | { |
| | 3 | 39 | | "accesstoken", |
| | 3 | 40 | | "apikey", |
| | 3 | 41 | | "authorization", |
| | 3 | 42 | | "bearer", |
| | 3 | 43 | | "connectionstring", |
| | 3 | 44 | | "credential", |
| | 3 | 45 | | "password", |
| | 3 | 46 | | "privatekey", |
| | 3 | 47 | | "refreshtoken", |
| | 3 | 48 | | "secret", |
| | 3 | 49 | | "socialsecurity", |
| | 3 | 50 | | "ssn" |
| | 3 | 51 | | }); |
| | | 52 | | |
| | 15 | 53 | | private GovernanceMetadataBudget( |
| | 15 | 54 | | int maxCount, |
| | 15 | 55 | | int maxKeyLength, |
| | 15 | 56 | | int maxValueLength, |
| | 15 | 57 | | int maxSerializedBytes, |
| | 15 | 58 | | IReadOnlyList<string> reservedKeyFragments) |
| | | 59 | | { |
| | 15 | 60 | | ThrowIfLessThanOne(maxCount, nameof(maxCount)); |
| | 13 | 61 | | ThrowIfLessThanOne(maxKeyLength, nameof(maxKeyLength)); |
| | 13 | 62 | | ThrowIfLessThanOne(maxValueLength, nameof(maxValueLength)); |
| | 13 | 63 | | ThrowIfLessThanOne(maxSerializedBytes, nameof(maxSerializedBytes)); |
| | 13 | 64 | | ArgumentNullException.ThrowIfNull(reservedKeyFragments); |
| | | 65 | | |
| | 13 | 66 | | MaxCount = maxCount; |
| | 13 | 67 | | MaxKeyLength = maxKeyLength; |
| | 13 | 68 | | MaxValueLength = maxValueLength; |
| | 13 | 69 | | MaxSerializedBytes = maxSerializedBytes; |
| | 13 | 70 | | ReservedKeyFragments = reservedKeyFragments; |
| | 13 | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Gets the recommended governance metadata budget. |
| | | 75 | | /// </summary> |
| | 24 | 76 | | public static GovernanceMetadataBudget Recommended { get; } = Create(); |
| | | 77 | | |
| | | 78 | | /// <summary> |
| | | 79 | | /// Gets the maximum number of normalized metadata entries allowed by the budget. |
| | | 80 | | /// </summary> |
| | 33 | 81 | | public int MaxCount { get; } |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Gets the maximum normalized metadata key length allowed by the budget, in characters. |
| | | 85 | | /// </summary> |
| | 26 | 86 | | public int MaxKeyLength { get; } |
| | | 87 | | |
| | | 88 | | /// <summary> |
| | | 89 | | /// Gets the maximum normalized metadata value length allowed by the budget, in characters. |
| | | 90 | | /// </summary> |
| | 30 | 91 | | public int MaxValueLength { get; } |
| | | 92 | | |
| | | 93 | | /// <summary> |
| | | 94 | | /// Gets the maximum estimated serialized metadata size allowed by the budget, in UTF-8 bytes. |
| | | 95 | | /// </summary> |
| | 31 | 96 | | public int MaxSerializedBytes { get; } |
| | | 97 | | |
| | | 98 | | /// <summary> |
| | | 99 | | /// Gets normalized reserved or discouraged metadata key fragments. |
| | | 100 | | /// </summary> |
| | 36 | 101 | | public IReadOnlyList<string> ReservedKeyFragments { get; } |
| | | 102 | | |
| | | 103 | | /// <summary> |
| | | 104 | | /// Creates a governance metadata budget. |
| | | 105 | | /// </summary> |
| | | 106 | | public static GovernanceMetadataBudget Create( |
| | | 107 | | int maxCount = DefaultMaxCount, |
| | | 108 | | int maxKeyLength = DefaultMaxKeyLength, |
| | | 109 | | int maxValueLength = DefaultMaxValueLength, |
| | | 110 | | int maxSerializedBytes = DefaultMaxSerializedBytes, |
| | | 111 | | IEnumerable<string>? reservedKeyFragments = null) |
| | | 112 | | { |
| | 15 | 113 | | return new GovernanceMetadataBudget( |
| | 15 | 114 | | maxCount, |
| | 15 | 115 | | maxKeyLength, |
| | 15 | 116 | | maxValueLength, |
| | 15 | 117 | | maxSerializedBytes, |
| | 15 | 118 | | NormalizeReservedKeyFragments(reservedKeyFragments ?? DefaultReservedKeyFragments)); |
| | | 119 | | } |
| | | 120 | | |
| | | 121 | | internal static string NormalizeKeyForComparison(string key) |
| | | 122 | | { |
| | 76 | 123 | | string normalizedKey = key.Trim().ToLowerInvariant(); |
| | 76 | 124 | | char[] buffer = new char[normalizedKey.Length]; |
| | 76 | 125 | | int count = 0; |
| | | 126 | | |
| | 1530 | 127 | | foreach (char character in normalizedKey) |
| | | 128 | | { |
| | 689 | 129 | | if (char.IsLetterOrDigit(character)) |
| | | 130 | | { |
| | 679 | 131 | | buffer[count] = character; |
| | 679 | 132 | | count++; |
| | | 133 | | } |
| | | 134 | | } |
| | | 135 | | |
| | 76 | 136 | | return new string(buffer, 0, count); |
| | | 137 | | } |
| | | 138 | | |
| | | 139 | | private static ReadOnlyCollection<string> NormalizeReservedKeyFragments(IEnumerable<string> reservedKeyFragments) |
| | | 140 | | { |
| | 15 | 141 | | string[] normalizedFragments = [.. reservedKeyFragments |
| | 68 | 142 | | .Where(fragment => !string.IsNullOrWhiteSpace(fragment)) |
| | 15 | 143 | | .Select(NormalizeKeyForComparison) |
| | 66 | 144 | | .Where(fragment => fragment.Length > 0) |
| | 15 | 145 | | .Distinct(StringComparer.Ordinal) |
| | 79 | 146 | | .OrderBy(fragment => fragment, StringComparer.Ordinal)]; |
| | | 147 | | |
| | 15 | 148 | | return normalizedFragments.Length == 0 |
| | 15 | 149 | | ? Array.AsReadOnly(Array.Empty<string>()) |
| | 15 | 150 | | : Array.AsReadOnly(normalizedFragments); |
| | | 151 | | } |
| | | 152 | | |
| | | 153 | | private static void ThrowIfLessThanOne(int value, string parameterName) |
| | | 154 | | { |
| | 54 | 155 | | if (value < 1) |
| | | 156 | | { |
| | 2 | 157 | | throw new ArgumentOutOfRangeException(parameterName, value, "Value must be greater than or equal to one."); |
| | | 158 | | } |
| | 52 | 159 | | } |
| | | 160 | | } |