< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Domain.Entities.Person
Assembly: ArturRios.Heimdall.Domain
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Domain/ArturRios.Heimdall.Domain/Entities/Person.cs
Line coverage
97%
Covered lines: 43
Uncovered lines: 1
Coverable lines: 44
Total lines: 321
Line coverage: 97.7%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
97%
Covered methods: 43
Fully covered methods: 43
Total methods: 44
Method coverage: 97.7%
Full method coverage: 97.7%

Metrics

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Domain/ArturRios.Heimdall.Domain/Entities/Person.cs

#LineLine coverage
 1using System.ComponentModel.DataAnnotations;
 2using System.Linq.Expressions;
 3using ArturRios.Data.Relational.Core.Entities;
 4
 5namespace ArturRios.Heimdall.Domain.Entities;
 6
 7/// <summary>
 8///     A registered identity. Has no direct scope attribute — its relationship to scopes is
 9///     derived from its role: a <c>User</c> belongs to exactly one scope (via <c>SCOPE_USER</c>),
 10///     a <c>ScopeAdmin</c> owns one or more scopes (via <c>SCOPE_OWNER</c>), and a
 11///     <c>SystemAdmin</c> belongs to no scope.
 12/// </summary>
 13public class Person : Entity<long>
 14{
 15    /// <summary>
 16    ///     External identifier, generated on creation and used everywhere the person is addressed
 17    ///     from outside the database (API paths, response bodies, token claims). The internal
 18    ///     <see cref="Entity{TKey}.Id" /> is never exposed to callers.
 19    /// </summary>
 3885020    public Guid PublicId { get; set; } = Guid.NewGuid();
 21
 22    /// <summary>Full name. Required, max 200 characters.</summary>
 23    [MaxLength(200)]
 2579424    public string Name { get; set; } = string.Empty;
 25
 26    /// <summary>
 27    ///     Email address. Unique within the scope for <c>User</c>s (via <c>SCOPE_USER</c>);
 28    ///     unique system-wide for <c>ScopeAdmin</c>s and <c>SystemAdmin</c>s.
 29    /// </summary>
 30    [MaxLength(256)]
 2646531    public string Email { get; set; } = string.Empty;
 32
 33    /// <summary>Hash computed from the password and <see cref="Salt" />.</summary>
 1481734    public byte[] PasswordHash { get; set; } = [];
 35
 36    /// <summary>Randomly generated per person, used to hash the password.</summary>
 1479137    public byte[] Salt { get; set; } = [];
 38
 39    /// <summary>Logical deletion flag. Logically deleted persons are excluded from default queries.</summary>
 1344440    public bool IsDeleted { get; set; }
 41
 42    /// <summary>
 43    ///     When the record was logically deleted, or <c>null</c> while it is live. The retention
 44    ///     window that ends in anonymisation is measured from here (NFR-20).
 45    /// </summary>
 46    /// <remarks>
 47    ///     <see cref="UpdatedAt" /> cannot serve: it moves on any later write, so a record touched
 48    ///     after its deletion would have its window silently restarted. This one is written once, by
 49    ///     the logical deletion, and never again.
 50    /// </remarks>
 1045651    public DateTime? DeletedAt { get; set; }
 52
 53    /// <summary>
 54    ///     Why the record was deleted (see <c>DeletionKinds</c>), or <c>null</c> while it is live.
 55    ///     Decides which of the schedule's two deadlines applies.
 56    /// </summary>
 1026657    public int? DeletionKind { get; set; }
 58
 59    /// <summary>
 60    ///     When the record was anonymised, or <c>null</c> if it has not been. Set by the scheduled
 61    ///     pass, and the reason a second pass over the same row is a no-op rather than a repeated
 62    ///     overwrite.
 63    /// </summary>
 102764    public DateTime? AnonymisedAt { get; set; }
 65
 66    /// <summary>
 67    ///     When the data subject asked to be erased (UC-42), or <c>null</c> if they have not. Set
 68    ///     once, by the request, and never moved: the statutory deadline runs from the request, not
 69    ///     from any later event.
 70    /// </summary>
 47871    public DateTime? ErasureRequestedAt { get; set; }
 72
 73    /// <summary>
 74    ///     The deadline the erasure must not outlive, computed when the request is made.
 75    /// </summary>
 76    /// <remarks>
 77    ///     Stored rather than derived on read, and that is the point. GDPR Art. 12(3) attaches an
 78    ///     obligation at the moment of the request; recomputing it from the current configuration
 79    ///     would let a later change to the deadline move an obligation that had already attached —
 80    ///     forward, which is unlawful, or backward, which would silently mark existing requests
 81    ///     overdue.
 82    /// </remarks>
 34583    public DateTime? ErasureDueAt { get; set; }
 84
 85    /// <summary>
 86    ///     Why the erasure could not be carried out yet, or <c>null</c> if nothing blocks it. One of
 87    ///     the application's own canonical messages, never caller input.
 88    /// </summary>
 89    /// <remarks>
 90    ///     A blocked request is still a request: the deadline keeps running, and this is what makes
 91    ///     the reason visible to the administrator who has to clear it, rather than the request
 92    ///     failing and leaving no trace.
 93    /// </remarks>
 94    [MaxLength(500)]
 26195    public string? ErasureBlockedReason { get; set; }
 96
 97    /// <summary>Email verification status.</summary>
 1156898    public bool EmailVerified { get; set; }
 99
 100    /// <summary>
 101    ///     Consecutive failed password attempts since the last successful login. Reset to zero on
 102    ///     success, and the counter <see cref="LockedOutUntil" /> is derived from.
 103    /// </summary>
 597104    public int FailedLoginAttempts { get; set; }
 105
 106    /// <summary>
 107    ///     When set and still in the future, login is refused regardless of the password supplied —
 108    ///     the per-account half of the brute-force defence, alongside the per-IP request limiter. The
 109    ///     refusal is UC-11's ordinary <c>InvalidCredentials</c>, so a lockout is not observable to a
 110    ///     caller who does not already know the password.
 111    /// </summary>
 227112    public DateTime? LockedOutUntil { get; set; }
 113
 114    /// <summary>
 115    ///     Failed second-factor verifications at sign-in (UC-38) since the last successful one,
 116    ///     whatever challenge each was made against (FR-AU-12). A correct password does not reset it;
 117    ///     only a second factor that checks out does.
 118    /// </summary>
 119    /// <remarks>
 120    ///     This and the two columns after it are written only through <c>IAtomicWrites</c>, never by
 121    ///     a whole-row save: the mapping ignores them after insert, so a save of a copy read before a
 122    ///     failure was counted cannot write the count back down. The same holds for the three
 123    ///     re-authentication columns below.
 124    /// </remarks>
 535125    public int FailedSecondFactorAttempts { get; set; }
 126
 127    /// <summary>
 128    ///     When set and still in the future, every second factor presented at sign-in is refused,
 129    ///     right or wrong, with UC-38's ordinary refusal (FR-AU-12).
 130    /// </summary>
 192131    public DateTime? SecondFactorLockedOutUntil { get; set; }
 132
 133    /// <summary>
 134    ///     How many second-factor lockouts the account has had since its last successful second
 135    ///     factor. Each lockout lasts twice as long as the one before it, up to a cap (FR-AU-12).
 136    /// </summary>
 135137    public int SecondFactorLockouts { get; set; }
 138
 139    /// <summary>
 140    ///     Failed re-authentications — a password or a second factor checked under a bearer token, such
 141    ///     as disabling two-factor authentication or changing one's own email — in the window that
 142    ///     began at <see cref="ReauthenticationWindowStartedAt" /> (FR-AU-13). Kept apart from
 143    ///     <see cref="FailedLoginAttempts" />, so a stolen token cannot lock its owner out of signing in.
 144    /// </summary>
 343145    public int FailedReauthenticationAttempts { get; set; }
 146
 147    /// <summary>
 148    ///     When the first of the failures in <see cref="FailedReauthenticationAttempts" /> was counted, or
 149    ///     <c>null</c> when none is. A failure after the window has run out starts a new one.
 150    /// </summary>
 564151    public DateTime? ReauthenticationWindowStartedAt { get; set; }
 152
 153    /// <summary>
 154    ///     When set and still in the future, every re-authentication is refused, right or wrong, with
 155    ///     the same answer a wrong password or code gets (FR-AU-13).
 156    /// </summary>
 318157    public DateTime? ReauthenticationPausedUntil { get; set; }
 158
 159    /// <summary>
 160    ///     Account emails the person's own requests have had sent in the window ending at
 161    ///     <see cref="AccountEmailWindowEndsAt" /> (FR-PE-16): verification links they asked for again,
 162    ///     two-factor setup codes, and the notice an own email change sends to the previous address.
 163    ///     Written only by the targeted write that reserves a send, like the factor budgets.
 164    /// </summary>
 158165    public int AccountEmailsSent { get; set; }
 166
 167    /// <summary>
 168    ///     When the current one-day window of <see cref="AccountEmailsSent" /> ends, or <c>null</c>
 169    ///     before the first send. The first send after it ends opens a new window.
 170    /// </summary>
 234171    public DateTime? AccountEmailWindowEndsAt { get; set; }
 172
 173    /// <summary>
 174    ///     When the last of those sends was reserved, which the per-person cooldown is measured from
 175    ///     (FR-PE-16), or <c>null</c> before the first.
 176    /// </summary>
 230177    public DateTime? AccountEmailLastSentAt { get; set; }
 178
 179    /// <summary>
 180    ///     A random value every token issued to this person carries, and must still carry when it is
 181    ///     presented (FR-AU-11). Replaced whenever a credential changes — the password, the email, or
 182    ///     the second factor — which revokes every token issued before the change.
 183    /// </summary>
 184    /// <remarks>
 185    ///     Replaced only through <c>IAtomicWrites.RotateSecurityStampAsync</c>, and never written by a
 186    ///     whole-row save: the mapping ignores it after insert. A save of a copy read before a
 187    ///     rotation would otherwise write the old stamp back, and with it every token the rotation
 188    ///     revoked.
 189    /// </remarks>
 13624190    public Guid SecurityStamp { get; set; } = Guid.NewGuid();
 191
 192    /// <summary>Foreign key to the associated <c>Role</c> (internal Id). Required.</summary>
 13793193    public long RoleId { get; set; }
 194
 195    /// <summary>
 196    ///     The scope a <c>User</c> belongs to (internal Id), or <c>null</c> for a <c>ScopeAdmin</c>
 197    ///     or <c>SystemAdmin</c>. A copy of <see cref="ScopeMembership" />'s <c>ScopeId</c>, not a
 198    ///     second source of truth: <c>SCOPE_USER</c> remains the relationship (§4.6). A read that only
 199    ///     needs to know which scope a person is in may take this column directly — the email
 200    ///     verification and password-reset services do, to find the scope's link bases — because it
 201    ///     mirrors the membership at every place that writes either.
 202    /// </summary>
 203    /// <remarks>
 204    ///     This exists for one reason — FR-PE-09's per-scope rule cannot otherwise be enforced. The
 205    ///     scope lives in <c>SCOPE_USER</c> and the address in <c>PERSON</c>, and a PostgreSQL unique
 206    ///     index covers one table, so the rule was left to a check-then-insert that two concurrent
 207    ///     creates both pass. Carrying the scope here lets the index be written over columns this
 208    ///     table already has — <c>role_id</c>, <c>is_deleted</c>, <c>email</c> — with a condition
 209    ///     matching the application's check exactly, so nothing about the rule changes; only who
 210    ///     enforces it does.
 211    ///
 212    ///     Kept in step with <see cref="ScopeMembership" /> at the three places that write it:
 213    ///     UC-06 path a sets both, UC-23 and UC-08's role change clear both.
 214    /// </remarks>
 11239215    public long? ScopeId { get; set; }
 216
 217
 218    /// <summary>
 219    ///     The lawful basis this identity's data is processed on (see <c>LegalBases</c>), recorded
 220    ///     when the identity was created (GDPR Art. 5(2), LGPD Art. 8 §2).
 221    /// </summary>
 86222    public int LegalBasis { get; set; }
 223
 224    /// <summary>
 225    ///     The version of the privacy notice in force when the identity was created, or <c>null</c>
 226    ///     for identities predating the mechanism.
 227    /// </summary>
 228    /// <remarks>
 229    ///     Stored rather than looked up, for the same reason the erasure deadline is: what the
 230    ///     person was told is a fact about a moment, and reading the current version later would
 231    ///     answer a different question — what they would be told today.
 232    /// </remarks>
 233    [MaxLength(50)]
 80234    public string? PrivacyNoticeVersion { get; set; }
 235
 236    /// <summary>When the basis and notice version were recorded.</summary>
 81237    public DateTime? BasisRecordedAt { get; set; }
 238
 239    /// <summary>
 240    ///     When processing of this identity was restricted (GDPR Art. 18, LGPD Art. 18 III–IV), or
 241    ///     <c>null</c> if it is not restricted.
 242    /// </summary>
 243    /// <remarks>
 244    ///     Independent of <see cref="IsDeleted" />, and necessarily so. Deletion means the identity
 245    ///     is on its way out; restriction means it is disputed and must be preserved exactly as it
 246    ///     is while that is resolved. Overloading the deletion flag would make a restricted record
 247    ///     disappear from the administrator's view that has to resolve the dispute.
 248    /// </remarks>
 1985249    public DateTime? ProcessingRestrictedAt { get; set; }
 250
 251    /// <summary>
 252    ///     Which of Art. 18(1)'s four grounds the restriction rests on (see
 253    ///     <c>RestrictionGrounds</c>), or <c>null</c> if not restricted.
 254    /// </summary>
 921255    public int? RestrictionGround { get; set; }
 256
 257    /// <summary>
 258    ///     When the subject was told the restriction was about to be lifted, or <c>null</c>.
 259    /// </summary>
 260    /// <remarks>
 261    ///     GDPR Art. 18(3) requires the subject be informed <em>before</em> a restriction imposed at
 262    ///     their request is lifted. This records that it happened, and the lift refuses to proceed
 263    ///     without it.
 264    /// </remarks>
 24265    public DateTime? RestrictionLiftNotifiedAt { get; set; }
 266
 267
 268    /// <summary>Creation timestamp.</summary>
 213269    public DateTime CreatedAt { get; set; }
 270
 271    /// <summary>Last update timestamp.</summary>
 449272    public DateTime UpdatedAt { get; set; }
 273
 274    // Navigation properties
 275
 276    /// <summary>The role that classifies this person.</summary>
 0277    public Role Role { get; set; } = null!;
 278
 279    /// <summary>
 280    ///     Scope-ownership join rows. Non-empty for a <c>ScopeAdmin</c> (one per owned scope), empty
 281    ///     for a <c>User</c>; a <c>SystemAdmin</c> may hold any number (FR-SC-08, FR-PE-10).
 282    /// </summary>
 14261283    public ICollection<ScopeOwner> ScopeOwnerships { get; set; } = new List<ScopeOwner>();
 284
 285    /// <summary>
 286    ///     Scope-membership join row. Present for a <c>User</c> (exactly one); <c>null</c> for a
 287    ///     <c>ScopeAdmin</c> or <c>SystemAdmin</c>.
 288    /// </summary>
 811289    public ScopeUser? ScopeMembership { get; set; }
 290
 291    /// <summary>Applications owned by this person.</summary>
 13038292    public ICollection<Application> OwnedApplications { get; set; } = new List<Application>();
 293
 294    /// <summary>Password reset tokens issued for this person.</summary>
 13038295    public ICollection<PasswordResetToken> PasswordResetTokens { get; set; } = new List<PasswordResetToken>();
 296
 297    /// <summary>Email verification tokens issued for this person.</summary>
 13038298    public ICollection<EmailVerificationToken> EmailVerificationTokens { get; set; } = new List<EmailVerificationToken>(
 299
 300    /// <summary>
 301    ///     This person's two-factor authentication configuration (UC-36 – UC-40). <c>null</c> until
 302    ///     UC-36 initiates setup.
 303    /// </summary>
 83304    public TwoFactorAuth? TwoFactorAuth { get; set; }
 305
 306    /// <summary>
 307    ///     The one definition of an <em>active</em> person, as an expression a query can filter on in the
 308    ///     database: not logically deleted, no erasure requested, and processing not restricted. It is
 309    ///     who counts as a live co-owner when NFR-12 decides whether a scope would be left without an
 310    ///     owner (#211).
 311    /// </summary>
 312    /// <remarks>
 313    ///     A suspended person cannot authenticate, and neither in effect can the other two: a restricted
 314    ///     person's tokens are refused on every action but the data-subject ones (<c>ActorLivenessFilter</c>)
 315    ///     and the scope's owner list hides them, and a person whose erasure is requested is either
 316    ///     suspended already or waiting only for the block on that suspension to clear. None of them can
 317    ///     administer the scope, so none of them keeps it owned.
 318    /// </remarks>
 92319    public static Expression<Func<Person, bool>> Active { get; } =
 2320        person => !person.IsDeleted && person.ErasureRequestedAt == null && person.ProcessingRestrictedAt == null;
 321}