< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Retention.IdentityAnonymisationService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Retention/IdentityAnonymisationService.cs
Line coverage
96%
Covered lines: 27
Uncovered lines: 1
Coverable lines: 28
Total lines: 75
Line coverage: 96.4%
Branch coverage
85%
Covered branches: 12
Total branches: 14
Branch coverage: 85.7%
Method coverage
80%
Covered methods: 4
Fully covered methods: 4
Total methods: 5
Method coverage: 80%
Full method coverage: 80%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_PassName()100%210%
get_StartupDescription()100%11100%
RunAsync()85.71%1414100%
Ledger(...)100%11100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Retention/IdentityAnonymisationService.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Input;
 2using ArturRios.Heimdall.Command.Output;
 3using ArturRios.Heimdall.Shared.Retention;
 4using ArturRios.Mediator.Command;
 5
 6namespace 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>
 16public class IdentityAnonymisationService(
 17    IServiceScopeFactory scopeFactory,
 18    DataRetentionOptions retention,
 19    ILogger<IdentityAnonymisationService> logger)
 97020    : ScheduledPassService(scopeFactory, retention.PurgeInterval, logger)
 21{
 022    public override string PassName => "identity-anonymisation";
 23
 24    protected override string StartupDescription =>
 96625        $"Identity anonymisation scheduled every {retention.PurgeInterval}, anonymising records " +
 96626        $"{retention.SubjectErasureDeadline} after a requested erasure and " +
 96627        $"{retention.AdministrativeDeletionWindow} after an administrative deletion";
 28
 29    protected override async Task<ScheduledPassResult> RunAsync(IServiceProvider scope)
 30    {
 431        var mediator = scope.GetRequiredService<CommandMediator>();
 32
 433        var result = await mediator
 434            .ExecuteCommandAsync<AnonymiseExpiredDeletionsCommand, AnonymiseExpiredDeletionsCommandOutput>(
 435                new AnonymiseExpiredDeletionsCommand());
 36
 437        if (!result.Success)
 38        {
 39            // A retry of a blocked request can fail after the due identities were anonymised (#210); the
 40            // ledger still has to be written then, or those erasures would leave no trace outside the
 41            // database.
 242            return result.Data is { TotalAnonymised: > 0 } done
 243                ? new ScheduledPassResult(
 244                    $"Identity anonymisation failed: {string.Join("; ", result.Errors)}. " +
 245                    $"Anonymised {done.TotalAnonymised} identities nonetheless. {Ledger(done)}",
 246                    true, done.TotalAnonymised)
 247                : ScheduledPassResult.Failure("Identity anonymisation", result.Errors);
 48        }
 49
 250        var anonymised = result.Data!;
 51
 52        // The identifiers are named, not just counted: this line is the erasure ledger (NFR-25),
 53        // and it is what makes a restore reconcilable. A backup taken before a subject asked
 54        // reinstates them with no trace of the request, so the list has to survive somewhere the
 55        // restore cannot roll back — which means outside the database, in the logs.
 56        //
 57        // Safe to write and to ship off the host precisely because these identities are now
 58        // anonymised: a PublicId that resolves to nobody is not personal data.
 59        //
 60        // The identities still due are what the run left at its batch cap (#206): a number that does not
 61        // fall from run to run is a backlog that is not draining, and deadlines passing.
 262        return anonymised.TotalAnonymised == 0 && anonymised.Backlog == 0
 263            ? ScheduledPassResult.Quiet
 264            : new ScheduledPassResult(
 265                $"Anonymised {anonymised.TotalAnonymised} identities past their retention window: " +
 266                $"{anonymised.PersonsAnonymised} persons, {anonymised.GoogleUsersAnonymised} Google Users, " +
 267                $"removing {anonymised.DependentsRemoved} dependent rows; " +
 268                $"{anonymised.Backlog?.ToString() ?? "an unknown number"} still due after this run. " +
 269                Ledger(anonymised),
 270                false, anonymised.TotalAnonymised);
 471    }
 72
 73    private static string Ledger(AnonymiseExpiredDeletionsCommandOutput anonymised) =>
 374        $"Erasure ledger: {string.Join(",", anonymised.AnonymisedIds)}";
 75}