| | | 1 | | using ArturRios.Heimdall.Command.Input; |
| | | 2 | | using ArturRios.Heimdall.Command.Output; |
| | | 3 | | using ArturRios.Heimdall.Shared.Retention; |
| | | 4 | | using ArturRios.Mediator.Command; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Heimdall.WebApi.Retention; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Runs <see cref="PurgeExpiredTokensCommand" /> on an interval, so the single-use token |
| | | 10 | | /// retention period of NFR-19 is enforced without anyone having to call anything. |
| | | 11 | | /// </summary> |
| | | 12 | | public class TokenRetentionService( |
| | | 13 | | IServiceScopeFactory scopeFactory, |
| | | 14 | | DataRetentionOptions retention, |
| | | 15 | | ILogger<TokenRetentionService> logger) |
| | 968 | 16 | | : ScheduledPassService(scopeFactory, retention.PurgeInterval, logger) |
| | | 17 | | { |
| | 0 | 18 | | public override string PassName => "token-retention"; |
| | | 19 | | |
| | | 20 | | protected override string StartupDescription => |
| | 966 | 21 | | $"Token retention purge scheduled every {retention.PurgeInterval}, removing tokens more than " + |
| | 966 | 22 | | $"{retention.SingleUseTokenGrace} past expiry and scope owner invitations " + |
| | 966 | 23 | | $"{retention.ScopeInvitationRetention} after they ended or expired"; |
| | | 24 | | |
| | | 25 | | protected override async Task<ScheduledPassResult> RunAsync(IServiceProvider scope) |
| | | 26 | | { |
| | 2 | 27 | | var mediator = scope.GetRequiredService<CommandMediator>(); |
| | | 28 | | |
| | 2 | 29 | | var result = await mediator |
| | 2 | 30 | | .ExecuteCommandAsync<PurgeExpiredTokensCommand, PurgeExpiredTokensCommandOutput>( |
| | 2 | 31 | | new PurgeExpiredTokensCommand()); |
| | | 32 | | |
| | 2 | 33 | | if (!result.Success) |
| | | 34 | | { |
| | 1 | 35 | | return ScheduledPassResult.Failure("Token retention purge", result.Errors, result.Data?.TotalPurged ?? 0); |
| | | 36 | | } |
| | | 37 | | |
| | 1 | 38 | | var purged = result.Data!; |
| | | 39 | | |
| | 1 | 40 | | return new ScheduledPassResult(Summarise(purged), false, purged.TotalPurged); |
| | 2 | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// The line logged for a run: <c>null</c> when nothing was removed and nothing is left, otherwise |
| | | 45 | | /// the total, the rows still due after the run, and every term of the total. |
| | | 46 | | /// </summary> |
| | | 47 | | /// <remarks> |
| | | 48 | | /// A term left out makes the parts disagree with the total (#225): a run that purged only |
| | | 49 | | /// application credentials once logged "removed 3 rows" over a list of zeroes. The rows still due |
| | | 50 | | /// are what a run left at its batch cap (#206); a number that does not fall from run to run is a |
| | | 51 | | /// backlog that is not draining. |
| | | 52 | | /// </remarks> |
| | | 53 | | public static string? Summarise(PurgeExpiredTokensCommandOutput purged) => |
| | 4 | 54 | | purged.TotalPurged == 0 && purged.Backlog == 0 |
| | 4 | 55 | | ? null |
| | 4 | 56 | | : $"Token retention purge removed {purged.TotalPurged} rows " + |
| | 4 | 57 | | $"({purged.Backlog} still due after this run): " + |
| | 4 | 58 | | $"{purged.PasswordResetTokensPurged} password reset, " + |
| | 4 | 59 | | $"{purged.EmailVerificationTokensPurged} email verification, " + |
| | 4 | 60 | | $"{purged.TwoFactorEmailCodesPurged} two-factor email, " + |
| | 4 | 61 | | $"{purged.PendingRegistrationsPurged} pending registration, " + |
| | 4 | 62 | | $"{purged.ApplicationCredentialsPurged} application credential, " + |
| | 4 | 63 | | $"{purged.AttemptNoticeBudgetsPurged} attempt-notice budget, " + |
| | 4 | 64 | | $"{purged.GoogleIdTokenUsesPurged} used Google ID token, " + |
| | 4 | 65 | | $"{purged.ScopeOwnerInvitationsPurged} scope owner invitation"; |
| | | 66 | | } |