< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.DataExportDisclosure
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/DataExportDisclosure.cs
Line coverage
100%
Covered lines: 72
Uncovered lines: 0
Coverable lines: 72
Total lines: 134
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Recipients()100%11100%
get_Sources()100%11100%
get_Retention()100%11100%
get_Withheld()100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/DataExportDisclosure.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Output;
 2
 3namespace ArturRios.Heimdall.Command.Services;
 4
 5/// <summary>
 6///     The context Art. 15 requires an export to carry alongside the data: who else receives it,
 7///     how long each category is kept, and what is held but not reproduced.
 8/// </summary>
 9/// <remarks>
 10///     <para>
 11///         This duplicates the Data Protection Document and the Data Retention Schedule, which is a
 12///         real cost and is accepted deliberately: the subject receives one document, and a copy
 13///         that told them to go and read a repository would not be the "concise, transparent,
 14///         intelligible" form Art. 12(1) asks for.
 15///     </para>
 16///     <para>
 17///         The duplication is guarded rather than trusted. <c>DataExportDisclosureTests</c> asserts
 18///         that every recipient named here appears in the record of processing, so the two cannot
 19///         drift apart silently — which is the only failure mode duplication of this kind has.
 20///     </para>
 21/// </remarks>
 22public static class DataExportDisclosure
 23{
 24    /// <summary>Most audit entries a single export reproduces.</summary>
 25    /// <remarks>
 26    ///     A bound is needed — an old, busy account could otherwise produce a response measured in
 27    ///     megabytes — but a silently truncated copy is not the copy Art. 15 asks for, so reaching
 28    ///     it is reported on the document itself.
 29    /// </remarks>
 30    public const int MaximumAuditEntries = 5000;
 31
 32    /// <summary>Who else the subject's data reaches (GDPR Art. 15(1)(c), LGPD Art. 18 VII).</summary>
 33    public static IEnumerable<DataExportRecipient> Recipients =>
 2534    [
 2535        new()
 2536        {
 2537            Name = "Mailgun",
 2538            Purpose = "Delivering verification, password reset and two-factor emails",
 2539            Location = "United States"
 2540        },
 2541        new()
 2542        {
 2543            Name = "The organisation whose system this account belongs to",
 2544            Purpose = "Administering the identities within its own scope",
 2545            Location = "Varies by tenant"
 2546        },
 2547        new()
 2548        {
 2549            Name = "Hosting provider",
 2550            Purpose = "Storing the database",
 2551            Location = "Brazil"
 2552        }
 2553    ];
 54
 55    /// <summary>
 56    ///     Where data that did not come from the subject came from (GDPR Art. 14(2)(f) and Art.
 57    ///     15(1)(g), LGPD Art. 9).
 58    /// </summary>
 59    /// <remarks>
 60    ///     Google was listed as a <em>recipient</em> until the transfer assessment was corrected,
 61    ///     and it is not one: the ID token is validated offline against cached public certificates
 62    ///     and nothing about the person is ever sent to Google. It belongs here, where the direction
 63    ///     is right — and telling the subject where their data came from is an obligation in its own
 64    ///     right when it did not come from them.
 65    /// </remarks>
 66    public static IEnumerable<DataExportSource> Sources =>
 2567    [
 2568        new()
 2569        {
 2570            Name = "Google",
 2571            Provides =
 2572                "Your name, email address, profile picture and Google account identifier, if you "
 2573                + "signed in with Google. They come from the token Google issues when you sign in.",
 2574            DataSentThere =
 2575                "Nothing. The token is checked against Google's published certificates on this "
 2576                + "server; it is never sent to Google."
 2577        }
 2578    ];
 79
 80    /// <summary>How long each category is kept (GDPR Art. 15(1)(d)).</summary>
 81    public static IEnumerable<DataExportRetention> Retention =>
 2582    [
 2583        new() { Category = "Account", Period = "While the account exists" },
 2584        new() { Category = "Account, after you ask to be erased", Period = "Anonymised within 30 days" },
 2585        new()
 2586        {
 2587            Category = "Account, after an administrator deletes it",
 2588            Period = "Anonymised after 90 days, so a mistake can be undone"
 2589        },
 2590        new() { Category = "Password reset and verification links", Period = "A week after they expire" },
 2591        new()
 2592        {
 2593            Category = "Records of actions on the account",
 2594            Period = "18 months, then stripped of anything identifying you"
 2595        },
 2596        new() { Category = "Application logs", Period = "12 months" }
 2597    ];
 98
 99    /// <summary>
 100    ///     Values held about the subject that this document names but does not reproduce.
 101    /// </summary>
 102    /// <remarks>
 103    ///     Art. 15(4): the right to a copy must not adversely affect the rights of others — and
 104    ///     reproducing a password hash or a TOTP secret would hand whoever holds the token the
 105    ///     material to attack the account offline, which adversely affects the subject most of all.
 106    ///     Naming them is the compromise: the subject learns what exists without the export becoming
 107    ///     the best thing to steal.
 108    /// </remarks>
 109    public static IEnumerable<DataExportWithheld> Withheld =>
 25110    [
 25111        new()
 25112        {
 25113            Value = "Password hash and salt",
 25114            Reason = "Reproducing them would let anyone holding this document attack the password offline. "
 25115                     + "The password itself is not stored and cannot be recovered from what is."
 25116        },
 25117        new()
 25118        {
 25119            Value = "Two-factor secret",
 25120            Reason = "Encrypted at rest and never returned after the response that first generated it. "
 25121                     + "Reproducing it would defeat the second factor entirely."
 25122        },
 25123        new()
 25124        {
 25125            Value = "Two-factor recovery codes",
 25126            Reason = "Stored only as hashes, so the codes themselves no longer exist to reproduce."
 25127        },
 25128        new()
 25129        {
 25130            Value = "Password reset and email verification tokens",
 25131            Reason = "Stored only as digests, and a live one would let this document be used to take over the account."
 25132        }
 25133    ];
 134}