| | | 1 | | using ArturRios.Mediator.Command; |
| | | 2 | | |
| | | 3 | | namespace ArturRios.Heimdall.Command.Output; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Everything Heimdall holds about one data subject (UC-41), in one structured document. |
| | | 7 | | /// </summary> |
| | | 8 | | /// <remarks> |
| | | 9 | | /// Art. 15 asks for a copy of the personal data <em>and</em> the context that makes it |
| | | 10 | | /// intelligible — the purposes, the recipients, the retention periods, the rights. A dump of |
| | | 11 | | /// table rows would satisfy the first half and none of the second, so the sections after |
| | | 12 | | /// <see cref="AuditEntries" /> are part of the answer rather than decoration. |
| | | 13 | | /// </remarks> |
| | | 14 | | public class DataExportCommandOutput : CommandOutput |
| | | 15 | | { |
| | | 16 | | /// <summary>When this copy was produced.</summary> |
| | | 17 | | public DateTime ExportedAt { get; set; } |
| | | 18 | | |
| | | 19 | | /// <summary>Who it is about, and the values that identify them.</summary> |
| | | 20 | | public required DataExportSubject Subject { get; set; } |
| | | 21 | | |
| | | 22 | | /// <summary>The state of the account's credentials, without the credentials.</summary> |
| | | 23 | | public required DataExportSecurity Security { get; set; } |
| | | 24 | | |
| | | 25 | | /// <summary>The lawful basis this data is held on, and what the subject was told.</summary> |
| | | 26 | | public required DataExportProcessing Processing { get; set; } |
| | | 27 | | |
| | | 28 | | /// <summary>Deletion and erasure state, where any applies.</summary> |
| | | 29 | | public DataExportErasure? Erasure { get; set; } |
| | | 30 | | |
| | | 31 | | /// <summary>Scopes the subject belongs to or owns, by public identifier.</summary> |
| | | 32 | | public IEnumerable<Guid> ScopeMemberships { get; set; } = []; |
| | | 33 | | |
| | | 34 | | /// <inheritdoc cref="ScopeMemberships" /> |
| | | 35 | | public IEnumerable<Guid> ScopeOwnerships { get; set; } = []; |
| | | 36 | | |
| | | 37 | | /// <summary>Applications the subject owns.</summary> |
| | | 38 | | public IEnumerable<DataExportApplication> Applications { get; set; } = []; |
| | | 39 | | |
| | | 40 | | /// <summary>Every audit entry still attributed to the subject.</summary> |
| | | 41 | | /// <remarks> |
| | | 42 | | /// Entries whose attribution has been cleared under NFR-21 are absent, and necessarily so: |
| | | 43 | | /// once cleared, nothing connects them to this person, which is the whole point of clearing |
| | | 44 | | /// them. <see cref="AuditEntriesTruncated" /> says whether the cap was reached. |
| | | 45 | | /// </remarks> |
| | | 46 | | public IEnumerable<DataExportAuditEntry> AuditEntries { get; set; } = []; |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// Whether more audit entries exist than this document carries. Disclosed rather than |
| | | 50 | | /// silently truncated: a copy that quietly omits part of the data is not the copy Art. 15 |
| | | 51 | | /// asks for. |
| | | 52 | | /// </summary> |
| | | 53 | | public bool AuditEntriesTruncated { get; set; } |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Values held about the subject that are deliberately not reproduced here, each with the |
| | | 57 | | /// reason. |
| | | 58 | | /// </summary> |
| | | 59 | | /// <remarks> |
| | | 60 | | /// Art. 15(4) says the right to a copy must not adversely affect others, and reproducing a |
| | | 61 | | /// password hash or a TOTP secret would hand an attacker holding a stolen token the material |
| | | 62 | | /// to attack the account offline. They are named rather than silently omitted, so the |
| | | 63 | | /// subject knows what exists. |
| | | 64 | | /// </remarks> |
| | | 65 | | public IEnumerable<DataExportWithheld> Withheld { get; set; } = []; |
| | | 66 | | |
| | | 67 | | /// <summary>Who else receives this data (GDPR Art. 15(1)(c), LGPD Art. 18 VII).</summary> |
| | | 68 | | public IEnumerable<DataExportRecipient> Recipients { get; set; } = []; |
| | | 69 | | |
| | | 70 | | /// <summary> |
| | | 71 | | /// Where data that did not come from the subject came from instead (GDPR Art. 14(2)(f) and |
| | | 72 | | /// Art. 15(1)(g), LGPD Art. 9). |
| | | 73 | | /// </summary> |
| | | 74 | | /// <remarks> |
| | | 75 | | /// Separate from <see cref="Recipients" /> because the direction is opposite and conflating |
| | | 76 | | /// them misleads in both directions. Google is the clearest case: it is where a Google |
| | | 77 | | /// User's name and address came from, and nothing about them is ever sent to it — the ID |
| | | 78 | | /// token is validated offline against cached public certificates. |
| | | 79 | | /// </remarks> |
| | | 80 | | public IEnumerable<DataExportSource> Sources { get; set; } = []; |
| | | 81 | | |
| | | 82 | | /// <summary>How long each category is kept (GDPR Art. 15(1)(d)).</summary> |
| | | 83 | | public IEnumerable<DataExportRetention> Retention { get; set; } = []; |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | /// <summary>The identifying values held about the subject.</summary> |
| | | 87 | | public class DataExportSubject |
| | | 88 | | { |
| | | 89 | | /// <summary>Public identifier. The internal database key is never exposed (NFR-15).</summary> |
| | | 90 | | public Guid Id { get; set; } |
| | | 91 | | |
| | | 92 | | /// <summary>Whether the subject authenticates with a password or through Google.</summary> |
| | | 93 | | public string Kind { get; set; } = string.Empty; |
| | | 94 | | |
| | | 95 | | public string Name { get; set; } = string.Empty; |
| | | 96 | | public string Email { get; set; } = string.Empty; |
| | | 97 | | public bool EmailVerified { get; set; } |
| | | 98 | | |
| | | 99 | | /// <summary>Role value, for a person; <c>null</c> for a Google User, who is always User-equivalent.</summary> |
| | | 100 | | public int? Role { get; set; } |
| | | 101 | | |
| | | 102 | | /// <summary>Google's stable subject identifier, for a Google User.</summary> |
| | | 103 | | public string? GoogleId { get; set; } |
| | | 104 | | |
| | | 105 | | /// <summary>Profile picture URL, for a Google User.</summary> |
| | | 106 | | public string? ProfilePictureUrl { get; set; } |
| | | 107 | | |
| | | 108 | | public bool IsDeleted { get; set; } |
| | | 109 | | public DateTime CreatedAt { get; set; } |
| | | 110 | | public DateTime UpdatedAt { get; set; } |
| | | 111 | | } |
| | | 112 | | |
| | | 113 | | /// <summary>The state of the account's credentials, described rather than disclosed.</summary> |
| | | 114 | | public class DataExportSecurity |
| | | 115 | | { |
| | | 116 | | /// <summary>Whether a password is set. A Google User has none.</summary> |
| | | 117 | | public bool PasswordSet { get; set; } |
| | | 118 | | |
| | | 119 | | /// <summary>Whether two-factor authentication is active.</summary> |
| | | 120 | | public bool TwoFactorActive { get; set; } |
| | | 121 | | |
| | | 122 | | /// <summary>Whether the authenticator-app method is enabled.</summary> |
| | | 123 | | public bool TwoFactorAppEnabled { get; set; } |
| | | 124 | | |
| | | 125 | | /// <summary>Whether the email method is enabled.</summary> |
| | | 126 | | public bool TwoFactorEmailEnabled { get; set; } |
| | | 127 | | |
| | | 128 | | /// <summary>Recovery codes issued and not yet used.</summary> |
| | | 129 | | public int UnusedRecoveryCodes { get; set; } |
| | | 130 | | |
| | | 131 | | /// <summary>Consecutive failed sign-ins since the last success.</summary> |
| | | 132 | | public int FailedLoginAttempts { get; set; } |
| | | 133 | | |
| | | 134 | | /// <summary>When the account is locked out until, if it is.</summary> |
| | | 135 | | public DateTime? LockedOutUntil { get; set; } |
| | | 136 | | |
| | | 137 | | /// <summary>Failed second factors at sign-in since the last successful one (FR-AU-12).</summary> |
| | | 138 | | public int FailedSecondFactorAttempts { get; set; } |
| | | 139 | | |
| | | 140 | | /// <summary>When second-factor verification is locked until, if it is or was (FR-AU-12).</summary> |
| | | 141 | | public DateTime? SecondFactorLockedOutUntil { get; set; } |
| | | 142 | | |
| | | 143 | | /// <summary>Second-factor locks since the last successful second factor, which set the next one's length.</summary> |
| | | 144 | | public int SecondFactorLockouts { get; set; } |
| | | 145 | | |
| | | 146 | | /// <summary>Failed re-authentications in the current window (FR-AU-13).</summary> |
| | | 147 | | public int FailedReauthenticationAttempts { get; set; } |
| | | 148 | | |
| | | 149 | | /// <summary>When the current re-authentication window began, if one is open (FR-AU-13).</summary> |
| | | 150 | | public DateTime? ReauthenticationWindowStartedAt { get; set; } |
| | | 151 | | |
| | | 152 | | /// <summary>When re-authentication is paused until, if it is or was (FR-AU-13).</summary> |
| | | 153 | | public DateTime? ReauthenticationPausedUntil { get; set; } |
| | | 154 | | |
| | | 155 | | /// <summary>Account emails the person's own requests had sent in the current window (FR-PE-16).</summary> |
| | | 156 | | public int AccountEmailsSent { get; set; } |
| | | 157 | | |
| | | 158 | | /// <summary>When the current account-email window ends, if one was opened (FR-PE-16).</summary> |
| | | 159 | | public DateTime? AccountEmailWindowEndsAt { get; set; } |
| | | 160 | | |
| | | 161 | | /// <summary>When the last of those account emails was sent, which the cooldown counts from (FR-PE-16).</summary> |
| | | 162 | | public DateTime? AccountEmailLastSentAt { get; set; } |
| | | 163 | | } |
| | | 164 | | |
| | | 165 | | /// <summary>The lawful basis and the notice the subject was shown.</summary> |
| | | 166 | | public class DataExportProcessing |
| | | 167 | | { |
| | | 168 | | /// <summary>The recorded basis value (see <c>LegalBases</c>).</summary> |
| | | 169 | | public int LegalBasis { get; set; } |
| | | 170 | | |
| | | 171 | | /// <summary>The basis in words, so the document is readable without the enum.</summary> |
| | | 172 | | public string LegalBasisName { get; set; } = string.Empty; |
| | | 173 | | |
| | | 174 | | /// <summary>The privacy notice version in force when the identity was created.</summary> |
| | | 175 | | public string? PrivacyNoticeVersion { get; set; } |
| | | 176 | | |
| | | 177 | | /// <summary>When the basis was recorded.</summary> |
| | | 178 | | public DateTime? RecordedAt { get; set; } |
| | | 179 | | } |
| | | 180 | | |
| | | 181 | | /// <summary>Deletion and erasure state.</summary> |
| | | 182 | | public class DataExportErasure |
| | | 183 | | { |
| | | 184 | | public bool IsDeleted { get; set; } |
| | | 185 | | public DateTime? DeletedAt { get; set; } |
| | | 186 | | public int? DeletionKind { get; set; } |
| | | 187 | | public DateTime? AnonymisedAt { get; set; } |
| | | 188 | | public DateTime? ErasureRequestedAt { get; set; } |
| | | 189 | | public DateTime? ErasureDueAt { get; set; } |
| | | 190 | | public string? ErasureBlockedReason { get; set; } |
| | | 191 | | } |
| | | 192 | | |
| | | 193 | | /// <summary>An application the subject owns.</summary> |
| | | 194 | | public class DataExportApplication |
| | | 195 | | { |
| | | 196 | | public Guid Id { get; set; } |
| | | 197 | | public string Name { get; set; } = string.Empty; |
| | | 198 | | public Guid ScopeId { get; set; } |
| | | 199 | | public bool IsDeleted { get; set; } |
| | | 200 | | } |
| | | 201 | | |
| | | 202 | | /// <summary>One audit entry attributed to the subject.</summary> |
| | | 203 | | public class DataExportAuditEntry |
| | | 204 | | { |
| | | 205 | | public Guid Id { get; set; } |
| | | 206 | | public string Action { get; set; } = string.Empty; |
| | | 207 | | public Guid? TargetId { get; set; } |
| | | 208 | | public bool Succeeded { get; set; } |
| | | 209 | | public string? FailureReason { get; set; } |
| | | 210 | | public DateTime CreatedAt { get; set; } |
| | | 211 | | } |
| | | 212 | | |
| | | 213 | | /// <summary>A value held but not reproduced, and why.</summary> |
| | | 214 | | public class DataExportWithheld |
| | | 215 | | { |
| | | 216 | | public string Value { get; set; } = string.Empty; |
| | | 217 | | public string Reason { get; set; } = string.Empty; |
| | | 218 | | } |
| | | 219 | | |
| | | 220 | | /// <summary>Somebody else this data reaches.</summary> |
| | | 221 | | public class DataExportRecipient |
| | | 222 | | { |
| | 264 | 223 | | public string Name { get; set; } = string.Empty; |
| | 261 | 224 | | public string Purpose { get; set; } = string.Empty; |
| | 261 | 225 | | public string Location { get; set; } = string.Empty; |
| | | 226 | | } |
| | | 227 | | |
| | | 228 | | /// <summary>Somewhere data about the subject came from, other than the subject.</summary> |
| | | 229 | | public class DataExportSource |
| | | 230 | | { |
| | | 231 | | public string Name { get; set; } = string.Empty; |
| | | 232 | | |
| | | 233 | | /// <summary>What came from there.</summary> |
| | | 234 | | public string Provides { get; set; } = string.Empty; |
| | | 235 | | |
| | | 236 | | /// <summary>Whether anything about the subject is sent back to it.</summary> |
| | | 237 | | public string DataSentThere { get; set; } = string.Empty; |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | /// <summary>How long a category of data is kept.</summary> |
| | | 241 | | public class DataExportRetention |
| | | 242 | | { |
| | | 243 | | public string Category { get; set; } = string.Empty; |
| | | 244 | | public string Period { get; set; } = string.Empty; |
| | | 245 | | } |