| | | 1 | | namespace ArturRios.Heimdall.Shared.Retention; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// The retention periods NFR-19 requires the API to enforce, read from the |
| | | 5 | | /// <c>HEIMDALL_RETENTION_*</c> environment variables. The full schedule — every table holding |
| | | 6 | | /// personal data, its period, and the reason for it — is the Data Retention Schedule Document; |
| | | 7 | | /// this type carries only the periods the API enforces itself, so nothing here promises a |
| | | 8 | | /// retention rule that no code applies. |
| | | 9 | | /// </summary> |
| | | 10 | | /// <remarks> |
| | | 11 | | /// <para> |
| | | 12 | | /// Every value has a default that is safe to run with, because a deployment that sets none |
| | | 13 | | /// of these variables must still enforce a period: an unset variable meaning "keep forever" |
| | | 14 | | /// is exactly the state NFR-19 exists to end. |
| | | 15 | | /// </para> |
| | | 16 | | /// <para> |
| | | 17 | | /// A malformed or out-of-range value falls back to the default rather than failing start-up. |
| | | 18 | | /// The alternative was considered and rejected: refusing to start would turn a typo in an |
| | | 19 | | /// operator's retention setting into an outage of an identity provider that every client |
| | | 20 | | /// system authenticates against, which is a far worse failure than running one interval on |
| | | 21 | | /// the documented default. The fallback is logged by the caller. |
| | | 22 | | /// </para> |
| | | 23 | | /// </remarks> |
| | | 24 | | public sealed record DataRetentionOptions |
| | | 25 | | { |
| | | 26 | | /// <summary>Environment variable holding the single-use token grace period, in days.</summary> |
| | | 27 | | public const string SingleUseTokenGraceDaysVariable = "HEIMDALL_RETENTION_TOKEN_GRACE_DAYS"; |
| | | 28 | | |
| | | 29 | | /// <summary>Environment variable holding the interval between purge runs, in minutes.</summary> |
| | | 30 | | public const string PurgeIntervalMinutesVariable = "HEIMDALL_RETENTION_PURGE_INTERVAL_MINUTES"; |
| | | 31 | | |
| | | 32 | | /// <summary>Environment variable holding the maximum rows removed from one table per run.</summary> |
| | | 33 | | public const string PurgeBatchSizeVariable = "HEIMDALL_RETENTION_PURGE_BATCH_SIZE"; |
| | | 34 | | |
| | | 35 | | /// <summary>Environment variable switching the scheduled purge off (<c>false</c> disables it).</summary> |
| | | 36 | | public const string PurgeEnabledVariable = "HEIMDALL_RETENTION_PURGE_ENABLED"; |
| | | 37 | | |
| | | 38 | | /// <summary>Environment variable holding the statutory erasure deadline, in days.</summary> |
| | | 39 | | public const string SubjectErasureDeadlineDaysVariable = "HEIMDALL_RETENTION_ERASURE_DEADLINE_DAYS"; |
| | | 40 | | |
| | | 41 | | /// <summary>Environment variable holding the administrative reversal window, in days.</summary> |
| | | 42 | | public const string AdministrativeDeletionWindowDaysVariable = "HEIMDALL_RETENTION_DELETION_WINDOW_DAYS"; |
| | | 43 | | |
| | | 44 | | /// <summary>Environment variable switching the scheduled anonymisation off.</summary> |
| | | 45 | | public const string AnonymisationEnabledVariable = "HEIMDALL_RETENTION_ANONYMISATION_ENABLED"; |
| | | 46 | | |
| | | 47 | | /// <summary>Environment variable holding how long an audit entry stays attributed, in days.</summary> |
| | | 48 | | public const string AuditActorRetentionDaysVariable = "HEIMDALL_RETENTION_AUDIT_ACTOR_DAYS"; |
| | | 49 | | |
| | | 50 | | /// <summary>Environment variable switching the scheduled audit pseudonymisation off.</summary> |
| | | 51 | | public const string AuditPseudonymisationEnabledVariable = "HEIMDALL_RETENTION_AUDIT_PSEUDONYMISATION_ENABLED"; |
| | | 52 | | |
| | | 53 | | /// <summary>Environment variable holding how long application log files are kept, in days.</summary> |
| | | 54 | | public const string LogRetentionDaysVariable = "HEIMDALL_RETENTION_LOG_DAYS"; |
| | | 55 | | |
| | | 56 | | /// <summary>Environment variable switching security signal detection off.</summary> |
| | | 57 | | public const string SecurityMonitoringEnabledVariable = "HEIMDALL_MONITORING_ENABLED"; |
| | | 58 | | |
| | | 59 | | /// <summary>Environment variable holding the window each signal is measured over, in minutes.</summary> |
| | | 60 | | public const string MonitoringWindowMinutesVariable = "HEIMDALL_MONITORING_WINDOW_MINUTES"; |
| | | 61 | | |
| | | 62 | | /// <summary>Environment variable holding how many refusals by one actor raise a signal.</summary> |
| | | 63 | | public const string RefusalThresholdVariable = "HEIMDALL_MONITORING_REFUSAL_THRESHOLD"; |
| | | 64 | | |
| | | 65 | | /// <summary>Environment variable holding how many locked-out accounts raise a signal.</summary> |
| | | 66 | | public const string LockoutThresholdVariable = "HEIMDALL_MONITORING_LOCKOUT_THRESHOLD"; |
| | | 67 | | |
| | | 68 | | /// <summary>Seven days — see <see cref="SingleUseTokenGrace" /> for why it is not zero.</summary> |
| | 4 | 69 | | public static readonly TimeSpan DefaultSingleUseTokenGrace = TimeSpan.FromDays(7); |
| | | 70 | | |
| | | 71 | | /// <summary>One hour. Nothing here is urgent; the grace period dwarfs the interval.</summary> |
| | 4 | 72 | | public static readonly TimeSpan DefaultPurgeInterval = TimeSpan.FromHours(1); |
| | | 73 | | |
| | | 74 | | /// <summary>Rows removed from one table in one run, by default.</summary> |
| | | 75 | | public const int DefaultPurgeBatchSize = 500; |
| | | 76 | | |
| | | 77 | | /// <summary>Thirty days — the outer limit GDPR Art. 12(3) allows, not a chosen period.</summary> |
| | 4 | 78 | | public static readonly TimeSpan DefaultSubjectErasureDeadline = TimeSpan.FromDays(30); |
| | | 79 | | |
| | | 80 | | /// <summary>Ninety days — the reversal window for an administrative deletion.</summary> |
| | 4 | 81 | | public static readonly TimeSpan DefaultAdministrativeDeletionWindow = TimeSpan.FromDays(90); |
| | | 82 | | |
| | | 83 | | /// <summary>548 days — eighteen months, the period the retention schedule sets.</summary> |
| | 4 | 84 | | public static readonly TimeSpan DefaultAuditActorRetention = TimeSpan.FromDays(548); |
| | | 85 | | |
| | | 86 | | /// <summary>365 days — twelve months, the period the retention schedule sets for logs.</summary> |
| | 4 | 87 | | public static readonly TimeSpan DefaultLogRetention = TimeSpan.FromDays(365); |
| | | 88 | | |
| | | 89 | | /// <summary>Fifteen minutes — long enough to see a pattern, short enough to still be news.</summary> |
| | 4 | 90 | | public static readonly TimeSpan DefaultMonitoringWindow = TimeSpan.FromMinutes(15); |
| | | 91 | | |
| | | 92 | | /// <summary>Refusals by one actor in the window before it is worth telling somebody.</summary> |
| | | 93 | | public const int DefaultRefusalThreshold = 20; |
| | | 94 | | |
| | | 95 | | /// <summary>Accounts locked out at once before it is worth telling somebody.</summary> |
| | | 96 | | public const int DefaultLockoutThreshold = 10; |
| | | 97 | | |
| | | 98 | | /// <summary>Bounds on the monitoring window, in minutes.</summary> |
| | | 99 | | private const double MinimumMonitoringWindowMinutes = 1; |
| | | 100 | | |
| | | 101 | | /// <inheritdoc cref="MinimumMonitoringWindowMinutes" /> |
| | | 102 | | private const double MaximumMonitoringWindowMinutes = 1440; |
| | | 103 | | |
| | | 104 | | /// <summary>Shortest accepted log retention, in days.</summary> |
| | | 105 | | private const double MinimumLogRetentionDays = 1; |
| | | 106 | | |
| | | 107 | | /// <summary>Longest accepted log retention, in days.</summary> |
| | | 108 | | private const double MaximumLogRetentionDays = 3650; |
| | | 109 | | |
| | | 110 | | /// <summary> |
| | | 111 | | /// Shortest accepted audit attribution retention, in days. Matches the floor the database |
| | | 112 | | /// trigger enforces: below it the pass would select rows the trigger then refuses, turning a |
| | | 113 | | /// configuration mistake into a run that fails every time rather than one that quietly does |
| | | 114 | | /// the wrong thing. |
| | | 115 | | /// </summary> |
| | | 116 | | private const double MinimumAuditActorRetentionDays = 30; |
| | | 117 | | |
| | | 118 | | /// <summary>Longest accepted audit attribution retention, in days.</summary> |
| | | 119 | | private const double MaximumAuditActorRetentionDays = 3650; |
| | | 120 | | |
| | | 121 | | /// <summary> |
| | | 122 | | /// Longest accepted erasure deadline, in days. Thirty is the statutory limit, so a value |
| | | 123 | | /// above it is not a policy choice but a compliance failure, and the ceiling refuses it. |
| | | 124 | | /// </summary> |
| | | 125 | | private const double MaximumErasureDeadlineDays = 30; |
| | | 126 | | |
| | | 127 | | /// <summary> |
| | | 128 | | /// Longest accepted administrative window, in days. Two years is already far past any |
| | | 129 | | /// reversal purpose; beyond it the window has stopped being a window. |
| | | 130 | | /// </summary> |
| | | 131 | | private const double MaximumDeletionWindowDays = 730; |
| | | 132 | | |
| | | 133 | | /// <summary> |
| | | 134 | | /// Longest accepted grace period, in days. Ten years is far past any purpose these tokens |
| | | 135 | | /// could serve, so a larger value is a typo — a stray unit, usually — rather than a policy, |
| | | 136 | | /// and honouring it would keep the data indefinitely under the appearance of a stated period. |
| | | 137 | | /// </summary> |
| | | 138 | | private const double MaximumGraceDays = 3650; |
| | | 139 | | |
| | | 140 | | /// <summary> |
| | | 141 | | /// Shortest accepted purge interval, in minutes. Keeps a mistyped value from turning the |
| | | 142 | | /// purge into continuous delete pressure on the tables the login path writes to. |
| | | 143 | | /// </summary> |
| | | 144 | | private const double MinimumIntervalMinutes = 1; |
| | | 145 | | |
| | | 146 | | /// <summary> |
| | | 147 | | /// Longest accepted purge interval, in minutes — seven days. |
| | | 148 | | /// </summary> |
| | | 149 | | /// <remarks> |
| | | 150 | | /// A ceiling is needed, not just tidy: <see cref="PeriodicTimer" /> refuses a period beyond |
| | | 151 | | /// roughly 49 days, and an interval it rejects would throw inside the hosted service, whose |
| | | 152 | | /// default behaviour on an unhandled exception is to stop the host. Without this, a stray |
| | | 153 | | /// digit in a retention setting would take the API down. |
| | | 154 | | /// </remarks> |
| | | 155 | | private const double MaximumIntervalMinutes = 7 * 24 * 60; |
| | | 156 | | |
| | | 157 | | /// <summary> |
| | | 158 | | /// How long a single-use token is kept after it expires, before the purge removes it |
| | | 159 | | /// (GDPR Art. 5(1)(e), LGPD Art. 15 III). |
| | | 160 | | /// </summary> |
| | | 161 | | /// <remarks> |
| | | 162 | | /// Deliberately not zero. UC-13 answers a presented token with three distinct outcomes — |
| | | 163 | | /// <c>TokenInvalid</c>, <c>TokenExpired</c>, <c>TokenAlreadyUsed</c> — and purging a row the |
| | | 164 | | /// moment it expires collapses the last two into the first, so a person following a stale |
| | | 165 | | /// link is told the token never existed rather than that it ran out. The grace period is |
| | | 166 | | /// what keeps that answer truthful for as long as anyone is plausibly still holding the |
| | | 167 | | /// link, and no longer. |
| | | 168 | | /// </remarks> |
| | 4694 | 169 | | public TimeSpan SingleUseTokenGrace { get; init; } = DefaultSingleUseTokenGrace; |
| | | 170 | | |
| | | 171 | | /// <summary>How often the scheduled purge runs.</summary> |
| | 9161 | 172 | | public TimeSpan PurgeInterval { get; init; } = DefaultPurgeInterval; |
| | | 173 | | |
| | | 174 | | /// <summary> |
| | | 175 | | /// The most rows the purge removes from any one table in a single run. |
| | | 176 | | /// </summary> |
| | | 177 | | /// <remarks> |
| | | 178 | | /// The bound is not a performance nicety. SRD §6.3.2 measured the write path degrading with |
| | | 179 | | /// the size of the table under sustained insert pressure, and a purge is sustained delete |
| | | 180 | | /// pressure on tables the login and recovery paths write to. Bounding the batch means a |
| | | 181 | | /// backlog drains over several runs instead of one long transaction competing with live |
| | | 182 | | /// traffic. |
| | | 183 | | /// </remarks> |
| | 4050 | 184 | | public int PurgeBatchSize { get; init; } = DefaultPurgeBatchSize; |
| | | 185 | | |
| | | 186 | | /// <summary> |
| | | 187 | | /// Whether the scheduled purge is registered at start-up. On by default: a deployment that |
| | | 188 | | /// sets nothing still enforces the schedule. |
| | | 189 | | /// </summary> |
| | 4664 | 190 | | public bool PurgeEnabled { get; init; } = true; |
| | | 191 | | |
| | | 192 | | /// <summary> |
| | | 193 | | /// How long a logically deleted identity is kept before anonymisation when the data subject |
| | | 194 | | /// asked to be erased (NFR-20). |
| | | 195 | | /// </summary> |
| | | 196 | | /// <remarks> |
| | | 197 | | /// An outer limit rather than a target. GDPR Art. 12(3) allows at most one month to act on |
| | | 198 | | /// an erasure request, so this is the deadline the erasure must not exceed — it completes as |
| | | 199 | | /// soon as it can. Raising it above thirty days is refused, because no configuration can |
| | | 200 | | /// make a longer period lawful. |
| | | 201 | | /// </remarks> |
| | 4758 | 202 | | public TimeSpan SubjectErasureDeadline { get; init; } = DefaultSubjectErasureDeadline; |
| | | 203 | | |
| | | 204 | | /// <summary> |
| | | 205 | | /// How long a logically deleted identity is kept before anonymisation when an administrator |
| | | 206 | | /// deleted it and no subject asked (NFR-20). |
| | | 207 | | /// </summary> |
| | | 208 | | /// <remarks> |
| | | 209 | | /// A reversal window: an administrator who deleted the wrong person needs to undo it, and |
| | | 210 | | /// the client systems in that person's scope need time to notice. Ninety days rather than |
| | | 211 | | /// the thirty most consumer services use, because a deletion here is not confined to one |
| | | 212 | | /// product — every client system authenticates against this one. |
| | | 213 | | /// </remarks> |
| | 4714 | 214 | | public TimeSpan AdministrativeDeletionWindow { get; init; } = DefaultAdministrativeDeletionWindow; |
| | | 215 | | |
| | | 216 | | /// <summary> |
| | | 217 | | /// Whether the scheduled anonymisation is registered at start-up. On by default, for the |
| | | 218 | | /// same reason the purge is. |
| | | 219 | | /// </summary> |
| | 4662 | 220 | | public bool AnonymisationEnabled { get; init; } = true; |
| | | 221 | | |
| | | 222 | | /// <summary> |
| | | 223 | | /// How long an audit entry stays attributed to the identity that produced it, before the |
| | | 224 | | /// attribution is cleared (NFR-21). |
| | | 225 | | /// </summary> |
| | | 226 | | /// <remarks> |
| | | 227 | | /// Eighteen months by default. Most security baselines settle on twelve — PCI DSS requires a |
| | | 228 | | /// year of audit history — and breach discovery lag routinely exceeds it, so twelve is a |
| | | 229 | | /// floor rather than a comfortable answer; eighteen covers a compliance cycle plus that lag. |
| | | 230 | | /// Past it the accountability value of <em>who</em> falls away sharply while the value of |
| | | 231 | | /// what happened, when and how often does not, and that half survives the attribution being |
| | | 232 | | /// cleared. |
| | | 233 | | /// </remarks> |
| | 4738 | 234 | | public TimeSpan AuditActorRetention { get; init; } = DefaultAuditActorRetention; |
| | | 235 | | |
| | | 236 | | /// <summary>Whether the scheduled audit pseudonymisation is registered at start-up.</summary> |
| | 4662 | 237 | | public bool AuditPseudonymisationEnabled { get; init; } = true; |
| | | 238 | | |
| | | 239 | | /// <summary> |
| | | 240 | | /// How long application log files are kept before the file sink removes them (NFR-22). |
| | | 241 | | /// </summary> |
| | | 242 | | /// <remarks> |
| | | 243 | | /// Twelve months, because these are the telemetry a breach is detected from rather than |
| | | 244 | | /// debugging output: breach discovery is measured in months, and a shorter period would |
| | | 245 | | /// delete the evidence before anyone knew to look. GDPR Art. 33's clock starts at awareness, |
| | | 246 | | /// which a log that had already expired never contributed to. |
| | | 247 | | /// </remarks> |
| | 4662 | 248 | | public TimeSpan LogRetention { get; init; } = DefaultLogRetention; |
| | | 249 | | |
| | | 250 | | /// <summary>Whether security signal detection is registered at start-up (NFR-26).</summary> |
| | 4662 | 251 | | public bool SecurityMonitoringEnabled { get; init; } = true; |
| | | 252 | | |
| | | 253 | | /// <summary> |
| | | 254 | | /// How far back each signal looks. A signal is about a rate, not a total: twenty refusals |
| | | 255 | | /// over a year is somebody who forgets their password, and twenty in a quarter of an hour is |
| | | 256 | | /// somebody trying things. |
| | | 257 | | /// </summary> |
| | 5591 | 258 | | public TimeSpan MonitoringWindow { get; init; } = DefaultMonitoringWindow; |
| | | 259 | | |
| | | 260 | | /// <summary>Refusals by one actor within the window before a signal is raised.</summary> |
| | 4693 | 261 | | public int RefusalThreshold { get; init; } = DefaultRefusalThreshold; |
| | | 262 | | |
| | | 263 | | /// <summary>Accounts locked out at once before a signal is raised.</summary> |
| | 4680 | 264 | | public int LockoutThreshold { get; init; } = DefaultLockoutThreshold; |
| | | 265 | | |
| | | 266 | | /// <summary> |
| | | 267 | | /// Names the variables whose values were unusable and fell back to a default, so the caller |
| | | 268 | | /// can say so in the log. Empty when every variable was absent or valid — an absent variable |
| | | 269 | | /// is not a fallback, it is the documented default being chosen. |
| | | 270 | | /// </summary> |
| | 4683 | 271 | | public IReadOnlyCollection<string> InvalidVariables { get; init; } = []; |
| | | 272 | | |
| | | 273 | | /// <summary>Reads the retention settings from the current process environment.</summary> |
| | | 274 | | public static DataRetentionOptions FromEnvironment() |
| | | 275 | | { |
| | 1830 | 276 | | var invalid = new List<string>(); |
| | | 277 | | |
| | 1830 | 278 | | return new DataRetentionOptions |
| | 1830 | 279 | | { |
| | 1830 | 280 | | SingleUseTokenGrace = ReadBoundedTimeSpan( |
| | 1830 | 281 | | SingleUseTokenGraceDaysVariable, DefaultSingleUseTokenGrace, TimeSpan.FromDays, |
| | 1830 | 282 | | minimum: 0, MaximumGraceDays, invalid), |
| | 1830 | 283 | | PurgeInterval = ReadBoundedTimeSpan( |
| | 1830 | 284 | | PurgeIntervalMinutesVariable, DefaultPurgeInterval, TimeSpan.FromMinutes, |
| | 1830 | 285 | | MinimumIntervalMinutes, MaximumIntervalMinutes, invalid), |
| | 1830 | 286 | | PurgeBatchSize = ReadPositiveInt(PurgeBatchSizeVariable, DefaultPurgeBatchSize, invalid), |
| | 1830 | 287 | | PurgeEnabled = ReadBool(PurgeEnabledVariable, defaultValue: true, invalid), |
| | 1830 | 288 | | SubjectErasureDeadline = ReadBoundedTimeSpan( |
| | 1830 | 289 | | SubjectErasureDeadlineDaysVariable, DefaultSubjectErasureDeadline, TimeSpan.FromDays, |
| | 1830 | 290 | | minimum: 0, MaximumErasureDeadlineDays, invalid), |
| | 1830 | 291 | | AdministrativeDeletionWindow = ReadBoundedTimeSpan( |
| | 1830 | 292 | | AdministrativeDeletionWindowDaysVariable, DefaultAdministrativeDeletionWindow, |
| | 1830 | 293 | | TimeSpan.FromDays, minimum: 0, MaximumDeletionWindowDays, invalid), |
| | 1830 | 294 | | AnonymisationEnabled = ReadBool(AnonymisationEnabledVariable, defaultValue: true, invalid), |
| | 1830 | 295 | | AuditActorRetention = ReadBoundedTimeSpan( |
| | 1830 | 296 | | AuditActorRetentionDaysVariable, DefaultAuditActorRetention, TimeSpan.FromDays, |
| | 1830 | 297 | | MinimumAuditActorRetentionDays, MaximumAuditActorRetentionDays, invalid), |
| | 1830 | 298 | | AuditPseudonymisationEnabled = ReadBool( |
| | 1830 | 299 | | AuditPseudonymisationEnabledVariable, defaultValue: true, invalid), |
| | 1830 | 300 | | LogRetention = ReadBoundedTimeSpan( |
| | 1830 | 301 | | LogRetentionDaysVariable, DefaultLogRetention, TimeSpan.FromDays, |
| | 1830 | 302 | | MinimumLogRetentionDays, MaximumLogRetentionDays, invalid), |
| | 1830 | 303 | | SecurityMonitoringEnabled = ReadBool( |
| | 1830 | 304 | | SecurityMonitoringEnabledVariable, defaultValue: true, invalid), |
| | 1830 | 305 | | MonitoringWindow = ReadBoundedTimeSpan( |
| | 1830 | 306 | | MonitoringWindowMinutesVariable, DefaultMonitoringWindow, TimeSpan.FromMinutes, |
| | 1830 | 307 | | MinimumMonitoringWindowMinutes, MaximumMonitoringWindowMinutes, invalid), |
| | 1830 | 308 | | RefusalThreshold = ReadPositiveInt(RefusalThresholdVariable, DefaultRefusalThreshold, invalid), |
| | 1830 | 309 | | LockoutThreshold = ReadPositiveInt(LockoutThresholdVariable, DefaultLockoutThreshold, invalid), |
| | 1830 | 310 | | InvalidVariables = invalid |
| | 1830 | 311 | | }; |
| | | 312 | | } |
| | | 313 | | |
| | | 314 | | /// <summary> |
| | | 315 | | /// Reads a period expressed in <paramref name="unit" />, accepting the raw number only when |
| | | 316 | | /// it is positive, at least <paramref name="minimum" />, and no greater than |
| | | 317 | | /// <paramref name="maximum" /> — both given in the variable's own unit, days or minutes. A |
| | | 318 | | /// <paramref name="minimum" /> of zero therefore means "more than 0", and any other is |
| | | 319 | | /// inclusive, which is how the Data Retention Schedule states each range ("1 to 10080", |
| | | 320 | | /// "30 to 3650"). It used to be exclusive throughout, so the documented lower bound of every |
| | | 321 | | /// range was itself refused. |
| | | 322 | | /// </summary> |
| | | 323 | | /// <remarks> |
| | | 324 | | /// The bounds are checked on the number, before it is converted, and that ordering is the |
| | | 325 | | /// point: <see cref="TimeSpan.FromDays" /> throws on a large enough <c>double</c>, so a |
| | | 326 | | /// check performed on the converted value would never run. An exception here would fail |
| | | 327 | | /// start-up, which is the one outcome this type exists to avoid. |
| | | 328 | | /// </remarks> |
| | | 329 | | private static TimeSpan ReadBoundedTimeSpan( |
| | | 330 | | string variable, |
| | | 331 | | TimeSpan defaultValue, |
| | | 332 | | Func<double, TimeSpan> unit, |
| | | 333 | | double minimum, |
| | | 334 | | double maximum, |
| | | 335 | | List<string> invalid) |
| | | 336 | | { |
| | 12810 | 337 | | var raw = Environment.GetEnvironmentVariable(variable); |
| | | 338 | | |
| | 12810 | 339 | | if (string.IsNullOrWhiteSpace(raw)) |
| | | 340 | | { |
| | 12782 | 341 | | return defaultValue; |
| | | 342 | | } |
| | | 343 | | |
| | | 344 | | // A zero or negative period would purge rows the moment they expire, or run continuously; |
| | | 345 | | // both are mistakes rather than policies. |
| | 28 | 346 | | if (!double.TryParse(raw, out var value) || value <= 0 || value < minimum || value > maximum) |
| | | 347 | | { |
| | 21 | 348 | | invalid.Add(variable); |
| | | 349 | | |
| | 21 | 350 | | return defaultValue; |
| | | 351 | | } |
| | | 352 | | |
| | 7 | 353 | | return unit(value); |
| | | 354 | | } |
| | | 355 | | |
| | | 356 | | private static int ReadPositiveInt(string variable, int defaultValue, List<string> invalid) |
| | | 357 | | { |
| | 5490 | 358 | | var raw = Environment.GetEnvironmentVariable(variable); |
| | | 359 | | |
| | 5490 | 360 | | if (string.IsNullOrWhiteSpace(raw)) |
| | | 361 | | { |
| | 5486 | 362 | | return defaultValue; |
| | | 363 | | } |
| | | 364 | | |
| | 4 | 365 | | if (!int.TryParse(raw, out var value) || value <= 0) |
| | | 366 | | { |
| | 3 | 367 | | invalid.Add(variable); |
| | | 368 | | |
| | 3 | 369 | | return defaultValue; |
| | | 370 | | } |
| | | 371 | | |
| | 1 | 372 | | return value; |
| | | 373 | | } |
| | | 374 | | |
| | | 375 | | private static bool ReadBool(string variable, bool defaultValue, List<string> invalid) |
| | | 376 | | { |
| | 7320 | 377 | | var raw = Environment.GetEnvironmentVariable(variable); |
| | | 378 | | |
| | 7320 | 379 | | if (string.IsNullOrWhiteSpace(raw)) |
| | | 380 | | { |
| | 7318 | 381 | | return defaultValue; |
| | | 382 | | } |
| | | 383 | | |
| | 2 | 384 | | if (!bool.TryParse(raw, out var value)) |
| | | 385 | | { |
| | 1 | 386 | | invalid.Add(variable); |
| | | 387 | | |
| | 1 | 388 | | return defaultValue; |
| | | 389 | | } |
| | | 390 | | |
| | 1 | 391 | | return value; |
| | | 392 | | } |
| | | 393 | | } |