| | | 1 | | using System.ComponentModel.DataAnnotations; |
| | | 2 | | using ArturRios.Data.Relational.Core.Entities; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.Domain.Entities; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// One audit trail entry per attempted write operation (NFR-09), whether it succeeded or not. |
| | | 8 | | /// <see cref="ActorPersonId" /> is a bare <c>PublicId</c>, not a foreign key, so an entry |
| | | 9 | | /// survives a hard-deleted person. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <para> |
| | | 13 | | /// <b>Append-only, with exactly two exceptions.</b> Database triggers refuse every |
| | | 14 | | /// <c>DELETE</c> and <c>TRUNCATE</c>, and refuse any <c>UPDATE</c> other than clearing |
| | | 15 | | /// <see cref="ActorPersonId" /> and <see cref="ActorRole" />, or clearing a |
| | | 16 | | /// <see cref="TargetId" /> that names a person or Google User. Each may be set to |
| | | 17 | | /// <c>null</c>, never to somebody else, and only once due (NFR-21). The resulting guarantee |
| | | 18 | | /// is precise: an action can never be denied, and who took it or whom it was taken on can |
| | | 19 | | /// never be reassigned — only forgotten, on schedule or on erasure. |
| | | 20 | | /// </para> |
| | | 21 | | /// <para> |
| | | 22 | | /// That exception exists because the trail holds personal data. An indefinitely retained, |
| | | 23 | | /// unalterable row naming a person who has exercised their right to erasure is personal data |
| | | 24 | | /// processed with no remaining basis. Clearing the attribution leaves what happened, when and |
| | | 25 | | /// how often intact while the entry stops relating to an identifiable person at all. |
| | | 26 | | /// </para> |
| | | 27 | | /// </remarks> |
| | | 28 | | public class AuditLog : Entity<long> |
| | | 29 | | { |
| | | 30 | | /// <summary>External identifier of this entry.</summary> |
| | 17418 | 31 | | public Guid PublicId { get; set; } = Guid.NewGuid(); |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// The acting person's <c>PublicId</c>; <c>null</c> for an anonymous write, and <c>null</c> |
| | | 35 | | /// again once the attribution has been cleared (NFR-21). |
| | | 36 | | /// </summary> |
| | | 37 | | /// <remarks> |
| | | 38 | | /// <para> |
| | | 39 | | /// This and <see cref="TargetId" /> are the columns on this entity that may change after |
| | | 40 | | /// creation, and they may only ever change to <c>null</c>. Everything else is immutable, |
| | | 41 | | /// and the row can never be deleted — see the remarks on the class. |
| | | 42 | | /// </para> |
| | | 43 | | /// <para> |
| | | 44 | | /// Two grounds make a clearing due: the entry is past the configured attribution period, |
| | | 45 | | /// or the identity it names has been anonymised. Both are enforced by the database, not |
| | | 46 | | /// here. |
| | | 47 | | /// </para> |
| | | 48 | | /// </remarks> |
| | 11431 | 49 | | public Guid? ActorPersonId { get; set; } |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// The acting person's role value (see <c>Roles</c>); <c>null</c> for an anonymous write, and |
| | | 53 | | /// cleared alongside <see cref="ActorPersonId" />. |
| | | 54 | | /// </summary> |
| | 1175 | 55 | | public int? ActorRole { get; set; } |
| | | 56 | | |
| | | 57 | | /// <summary>The command's CLR type name, e.g. <c>"CreateApplicationCommand"</c>.</summary> |
| | | 58 | | [MaxLength(200)] |
| | 17412 | 59 | | public string Action { get; set; } = string.Empty; |
| | | 60 | | |
| | | 61 | | /// <summary> |
| | | 62 | | /// Best-effort public identifier of the entity the write affected; <c>null</c> if none could be |
| | | 63 | | /// resolved, and <c>null</c> again once a person target has been cleared (NFR-21). |
| | | 64 | | /// </summary> |
| | | 65 | | /// <remarks> |
| | | 66 | | /// When this names a person or Google User the entry is personal data about them, whoever |
| | | 67 | | /// acted, so it is cleared on the actor's two grounds: past the attribution period, or once |
| | | 68 | | /// that identity is anonymised. It names one still after a hard delete, which tombstones the |
| | | 69 | | /// id first (<see cref="ErasedSubject" />, #195), so a tombstoned id with no row left is |
| | | 70 | | /// cleared too, at any age. A target naming anything else is not personal data and never |
| | | 71 | | /// changes. The database enforces all of it. |
| | | 72 | | /// </remarks> |
| | 6295 | 73 | | public Guid? TargetId { get; set; } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Whether the operation succeeded. The trail records refusals as well as writes, because a |
| | | 77 | | /// refused attempt is usually the more interesting one: a caller repeatedly denied a scope |
| | | 78 | | /// they do not own, or repeatedly failing a password, leaves no other trace. |
| | | 79 | | /// </summary> |
| | 11304 | 80 | | public bool Succeeded { get; set; } |
| | | 81 | | |
| | | 82 | | /// <summary> |
| | | 83 | | /// The first error the operation reported, when it failed; <c>null</c> on success. |
| | | 84 | | /// </summary> |
| | | 85 | | /// <remarks> |
| | | 86 | | /// One of the application's own canonical messages, or one of the persistence layer's |
| | | 87 | | /// classified failures — never a caller-supplied value and never provider text, so the trail |
| | | 88 | | /// cannot become a place where a submitted password or an address ends up recorded verbatim. |
| | | 89 | | /// </remarks> |
| | | 90 | | [MaxLength(500)] |
| | 6227 | 91 | | public string? FailureReason { get; set; } |
| | | 92 | | |
| | | 93 | | /// <summary>When the entry was written.</summary> |
| | 16506 | 94 | | public DateTime CreatedAt { get; set; } |
| | | 95 | | } |