| | | 1 | | using ArturRios.Data.Relational.Core.Entities; |
| | | 2 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 3 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | |
| | | 6 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 7 | | |
| | | 8 | | /// <summary> |
| | | 9 | | /// Permanently removes the rows that hold data belonging to persons whose identity is being |
| | | 10 | | /// anonymised: their password reset and email verification tokens, and their two-factor |
| | | 11 | | /// configuration — whose <c>ON DELETE CASCADE</c> foreign keys take the recovery codes and email |
| | | 12 | | /// codes with it. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// Shared by the scheduled anonymisation pass (NFR-20) and the re-application of erasures after |
| | | 16 | | /// a restore (NFR-25), so the two cannot disagree about what an erased person leaves behind. Only |
| | | 17 | | /// the first used to remove these rows; the second anonymised the person and kept the encrypted |
| | | 18 | | /// TOTP secret, and the scheduled pass never came back for it, since it selects only records not |
| | | 19 | | /// yet anonymised. |
| | | 20 | | /// </remarks> |
| | | 21 | | public static class PersonDependentsRemoval |
| | | 22 | | { |
| | | 23 | | /// <returns>How many rows the deletes reported removing, and any errors they returned.</returns> |
| | | 24 | | public static async Task<(int Removed, IReadOnlyList<string> Errors)> RemoveAsync( |
| | | 25 | | IReadOnlyCollection<long> personIds, |
| | | 26 | | IAsyncRepository<PasswordResetToken, long> passwordResetTokens, |
| | | 27 | | IAsyncRepository<EmailVerificationToken, long> emailVerificationTokens, |
| | | 28 | | IAsyncRepository<TwoFactorAuth, long> twoFactorAuths) |
| | | 29 | | { |
| | 24 | 30 | | if (personIds.Count == 0) |
| | | 31 | | { |
| | 3 | 32 | | return (0, []); |
| | | 33 | | } |
| | | 34 | | |
| | 21 | 35 | | var removed = 0; |
| | 21 | 36 | | var errors = new List<string>(); |
| | | 37 | | |
| | 21 | 38 | | var passwordResetTokenIds = await passwordResetTokens.Query() |
| | 21 | 39 | | .Where(token => personIds.Contains(token.PersonId)) |
| | 21 | 40 | | .Select(token => token.Id) |
| | 21 | 41 | | .ToListAsync(); |
| | | 42 | | |
| | 21 | 43 | | var emailVerificationTokenIds = await emailVerificationTokens.Query() |
| | 21 | 44 | | .Where(token => personIds.Contains(token.PersonId)) |
| | 21 | 45 | | .Select(token => token.Id) |
| | 21 | 46 | | .ToListAsync(); |
| | | 47 | | |
| | 21 | 48 | | var twoFactorAuthIds = await twoFactorAuths.Query() |
| | 21 | 49 | | .Where(configuration => personIds.Contains(configuration.PersonId)) |
| | 21 | 50 | | .Select(configuration => configuration.Id) |
| | 21 | 51 | | .ToListAsync(); |
| | | 52 | | |
| | 21 | 53 | | Collect(await DeleteAllAsync(passwordResetTokenIds, passwordResetTokens)); |
| | 21 | 54 | | Collect(await DeleteAllAsync(emailVerificationTokenIds, emailVerificationTokens)); |
| | 21 | 55 | | Collect(await DeleteAllAsync(twoFactorAuthIds, twoFactorAuths)); |
| | | 56 | | |
| | 21 | 57 | | return (removed, errors); |
| | | 58 | | |
| | | 59 | | void Collect((int Removed, IEnumerable<string> Errors) result) |
| | | 60 | | { |
| | 63 | 61 | | removed += result.Removed; |
| | 63 | 62 | | errors.AddRange(result.Errors); |
| | 63 | 63 | | } |
| | 24 | 64 | | } |
| | | 65 | | |
| | | 66 | | /// <summary> |
| | | 67 | | /// Permanently removes the entities with the given ids, or does nothing when there are none. |
| | | 68 | | /// Reports how many rows were actually removed, which is what the delete reported rather |
| | | 69 | | /// than what was selected. |
| | | 70 | | /// </summary> |
| | | 71 | | private static async Task<(int Removed, IEnumerable<string> Errors)> DeleteAllAsync<T>( |
| | | 72 | | IReadOnlyCollection<long> ids, IAsyncRepository<T, long> writer) where T : Entity<long> |
| | | 73 | | { |
| | 63 | 74 | | if (ids.Count == 0) |
| | | 75 | | { |
| | 54 | 76 | | return (0, []); |
| | | 77 | | } |
| | | 78 | | |
| | 9 | 79 | | var deletion = await writer.DeleteRangeAsync(ids); |
| | | 80 | | |
| | 9 | 81 | | return deletion.Success |
| | 9 | 82 | | ? (deletion.Data?.Count() ?? 0, []) |
| | 9 | 83 | | : (0, deletion.Errors); |
| | 63 | 84 | | } |
| | | 85 | | } |