| | | 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="AnonymiseExpiredDeletionsCommand" /> on an interval, so a logically deleted |
| | | 10 | | /// identity reaches a terminal state instead of sitting in the database indefinitely (NFR-20). |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks> |
| | | 13 | | /// This is the pass that actually discharges GDPR Art. 17 and LGPD Art. 16 for an identity, so |
| | | 14 | | /// unlike the token purge it logs every run that changed something, however small. |
| | | 15 | | /// </remarks> |
| | | 16 | | public class IdentityAnonymisationService( |
| | | 17 | | IServiceScopeFactory scopeFactory, |
| | | 18 | | DataRetentionOptions retention, |
| | | 19 | | ILogger<IdentityAnonymisationService> logger) |
| | 899 | 20 | | : ScheduledPassService(scopeFactory, retention.PurgeInterval, logger) |
| | | 21 | | { |
| | | 22 | | protected override string StartupDescription => |
| | 899 | 23 | | $"Identity anonymisation scheduled every {retention.PurgeInterval}, anonymising records " + |
| | 899 | 24 | | $"{retention.SubjectErasureDeadline} after a requested erasure and " + |
| | 899 | 25 | | $"{retention.AdministrativeDeletionWindow} after an administrative deletion"; |
| | | 26 | | |
| | | 27 | | protected override async Task<string?> RunAsync(IServiceProvider scope) |
| | | 28 | | { |
| | 0 | 29 | | var mediator = scope.GetRequiredService<CommandMediator>(); |
| | | 30 | | |
| | 0 | 31 | | var result = await mediator |
| | 0 | 32 | | .ExecuteCommandAsync<AnonymiseExpiredDeletionsCommand, AnonymiseExpiredDeletionsCommandOutput>( |
| | 0 | 33 | | new AnonymiseExpiredDeletionsCommand()); |
| | | 34 | | |
| | 0 | 35 | | if (!result.Success) |
| | | 36 | | { |
| | 0 | 37 | | return $"Identity anonymisation failed: {string.Join("; ", result.Errors)}"; |
| | | 38 | | } |
| | | 39 | | |
| | 0 | 40 | | var anonymised = result.Data!; |
| | | 41 | | |
| | | 42 | | // The identifiers are named, not just counted: this line is the erasure ledger (NFR-25), |
| | | 43 | | // and it is what makes a restore reconcilable. A backup taken before a subject asked |
| | | 44 | | // reinstates them with no trace of the request, so the list has to survive somewhere the |
| | | 45 | | // restore cannot roll back — which means outside the database, in the logs. |
| | | 46 | | // |
| | | 47 | | // Safe to write and to ship off the host precisely because these identities are now |
| | | 48 | | // anonymised: a PublicId that resolves to nobody is not personal data. |
| | 0 | 49 | | return anonymised.TotalAnonymised == 0 |
| | 0 | 50 | | ? null |
| | 0 | 51 | | : $"Anonymised {anonymised.TotalAnonymised} identities past their retention window: " + |
| | 0 | 52 | | $"{anonymised.PersonsAnonymised} persons, {anonymised.GoogleUsersAnonymised} Google Users, " + |
| | 0 | 53 | | $"removing {anonymised.DependentsRemoved} dependent rows. " + |
| | 0 | 54 | | $"Erasure ledger: {string.Join(",", anonymised.AnonymisedIds)}"; |
| | 0 | 55 | | } |
| | | 56 | | } |