| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 4 | | using ArturRios.Heimdall.Domain.Persistence; |
| | | 5 | | using Microsoft.EntityFrameworkCore; |
| | | 6 | | |
| | | 7 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// A person about to be taken out — deleted (UC-09, UC-10) or suspended for erasure (UC-42 and the |
| | | 11 | | /// anonymisation pass's retry) — as they stand once the invariant locks are held: their role, whether they |
| | | 12 | | /// are deleted, and the scopes they own. What <see cref="LastScopeOwnerGuard" /> and |
| | | 13 | | /// <see cref="LastSystemAdminGuard" /> decide on. |
| | | 14 | | /// </summary> |
| | | 15 | | /// <remarks> |
| | | 16 | | /// <para> |
| | | 17 | | /// <b>Read under the locks, not before them</b> (#202, #203). A copy read before the transaction can |
| | | 18 | | /// be stale by the time the guards ask: a scope the person owned may have been hard-deleted, so its row |
| | | 19 | | /// is gone from the lock and the guard would refuse for a scope that no longer exists; or the person may |
| | | 20 | | /// have been promoted to System Admin, and a role read before the lock would skip NFR-32's guard. |
| | | 21 | | /// </para> |
| | | 22 | | /// <para> |
| | | 23 | | /// <b>The System Admin set is locked for every one of them.</b> Which persons are administrators is |
| | | 24 | | /// only known under the lock, so the lock cannot be chosen by the role: a User or Scope Admin promoted |
| | | 25 | | /// while being deleted would otherwise be deleted as an administrator with no lock held. These writers |
| | | 26 | | /// are administrative and rare, so serialising them on one key costs nothing that matters. |
| | | 27 | | /// </para> |
| | | 28 | | /// <para> |
| | | 29 | | /// <b>An ownership gained after the locks</b> — a scope created naming the person (UC-03), an owner |
| | | 30 | | /// added (UC-21, UC-23) — is neither locked nor checked: the writers that add owners take no lock, since |
| | | 31 | | /// adding one can never leave a scope ownerless on its own. The System Requirements record that |
| | | 32 | | /// residual. |
| | | 33 | | /// </para> |
| | | 34 | | /// </remarks> |
| | | 35 | | /// <param name="RoleId">The person's role under the locks.</param> |
| | | 36 | | /// <param name="IsDeleted">Whether the person is logically deleted under the locks.</param> |
| | | 37 | | /// <param name="OwnedScopeIds">The scopes the person owns under the locks.</param> |
| | 275 | 38 | | public sealed record LockedPerson(long RoleId, bool IsDeleted, IReadOnlyCollection<long> OwnedScopeIds) |
| | | 39 | | { |
| | | 40 | | /// <summary>Whether the person is a System Admin NFR-32 protects: one not yet deleted.</summary> |
| | 60 | 41 | | public bool IsLiveSystemAdmin => RoleId == (long)Roles.SystemAdmin && !IsDeleted; |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Locks the scopes the person <paramref name="personId" /> owns, then the System Admin set — in the one |
| | | 45 | | /// <see cref="IInvariantLocks.LockAsync" /> call the lock order asks for, in the caller's transaction — |
| | | 46 | | /// and reads the person again under them. <c>null</c> when the person is gone. |
| | | 47 | | /// </summary> |
| | | 48 | | public static async Task<LockedPerson?> LockAsync( |
| | | 49 | | IInvariantLocks locks, IAsyncReadOnlyRepository<Person, long> personReader, long personId) |
| | | 50 | | { |
| | 89 | 51 | | var scopeIds = await personReader.Query() |
| | 89 | 52 | | .Where(person => person.Id == personId) |
| | 89 | 53 | | .SelectMany(person => person.ScopeOwnerships.Select(ownership => ownership.ScopeId)) |
| | 89 | 54 | | .ToListAsync(); |
| | | 55 | | |
| | 89 | 56 | | await locks.LockAsync(scopeIds, systemAdmins: true); |
| | | 57 | | |
| | | 58 | | // A projection, so the values are the database's as they stand now rather than a tracked copy's. |
| | | 59 | | // The ownerships are those the person still holds: a scope deleted since the read above is gone |
| | | 60 | | // from them, as it is from the lock. |
| | 89 | 61 | | var current = await personReader.Query() |
| | 89 | 62 | | .Where(person => person.Id == personId) |
| | 89 | 63 | | .Select(person => new |
| | 89 | 64 | | { |
| | 89 | 65 | | person.RoleId, |
| | 89 | 66 | | person.IsDeleted, |
| | 89 | 67 | | OwnedScopeIds = person.ScopeOwnerships.Select(ownership => ownership.ScopeId).ToList() |
| | 89 | 68 | | }) |
| | 89 | 69 | | .FirstOrDefaultAsync(); |
| | | 70 | | |
| | 89 | 71 | | return current is null ? null : new LockedPerson(current.RoleId, current.IsDeleted, current.OwnedScopeIds); |
| | 89 | 72 | | } |
| | | 73 | | } |