| | | 1 | | using ArturRios.Data.Relational.Core.Interfaces; |
| | | 2 | | using ArturRios.Heimdall.Domain.Entities; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | |
| | | 5 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// NFR-12: whether taking a person out of the scopes they own — removing one ownership (UC-22), |
| | | 9 | | /// deleting them (UC-09, UC-10), or suspending them for erasure (UC-42 and the anonymisation pass's |
| | | 10 | | /// retry) — would leave one of those scopes with no active owner. |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks> |
| | | 13 | | /// <para> |
| | | 14 | | /// <b>One guard for every writer.</b> UC-09, UC-10 and UC-22 used to carry their own copies; the |
| | | 15 | | /// definition of who keeps a scope owned now lives in one place, <see cref="Person.Active" /> |
| | | 16 | | /// (#211). |
| | | 17 | | /// </para> |
| | | 18 | | /// <para> |
| | | 19 | | /// <b>Who counts.</b> Only an active co-owner: not logically deleted, no erasure requested, and |
| | | 20 | | /// processing not restricted. A suspended owner cannot authenticate, a restricted one is refused |
| | | 21 | | /// on every action but the data-subject ones and hidden from the owner list, and one whose |
| | | 22 | | /// erasure is requested is on the way out — none of them can administer the scope, so none |
| | | 23 | | /// keeps it owned. |
| | | 24 | | /// </para> |
| | | 25 | | /// <para> |
| | | 26 | | /// <b>Asked under the scope locks (#202).</b> On its own the answer is stale the moment it is read: |
| | | 27 | | /// two requests taking out the two owners of one scope each read the other still there, both |
| | | 28 | | /// write, and nobody is left. Every caller therefore asks inside its transaction, once |
| | | 29 | | /// <c>IInvariantLocks</c> holds the scopes in question — <see cref="LockedPerson.LockAsync" /> |
| | | 30 | | /// for a person's — so a competing writer of the same scope waits until the answer has been acted |
| | | 31 | | /// on, and then reads what was written. |
| | | 32 | | /// </para> |
| | | 33 | | /// </remarks> |
| | | 34 | | public static class LastScopeOwnerGuard |
| | | 35 | | { |
| | | 36 | | /// <summary> |
| | | 37 | | /// Whether any of <paramref name="scopeIds" /> is owned by nobody active but the person |
| | | 38 | | /// <paramref name="personId" />. |
| | | 39 | | /// </summary> |
| | | 40 | | public static async Task<bool> WouldStripLastOwnerAsync( |
| | | 41 | | long personId, IReadOnlyCollection<long> scopeIds, IAsyncReadOnlyRepository<Person, long> personReader) |
| | | 42 | | { |
| | | 43 | | // Whatever the role: a SystemAdmin may own a scope too (FR-SC-08). |
| | 109 | 44 | | if (scopeIds.Count == 0) |
| | | 45 | | { |
| | 48 | 46 | | return false; |
| | | 47 | | } |
| | | 48 | | |
| | 61 | 49 | | var coOwnedScopeIds = await personReader.Query() |
| | 61 | 50 | | .Where(Person.Active) |
| | 61 | 51 | | .Where(other => other.Id != personId) |
| | 61 | 52 | | .SelectMany(other => other.ScopeOwnerships.Select(ownership => ownership.ScopeId)) |
| | 61 | 53 | | .Where(scopeId => scopeIds.Contains(scopeId)) |
| | 61 | 54 | | .Distinct() |
| | 61 | 55 | | .ToListAsync(); |
| | | 56 | | |
| | 124 | 57 | | return scopeIds.Any(scopeId => !coOwnedScopeIds.Contains(scopeId)); |
| | 109 | 58 | | } |
| | | 59 | | } |