| | | 1 | | using ArturRios.Heimdall.Data.Configuration; |
| | | 2 | | using ArturRios.Heimdall.Data.Persistence; |
| | | 3 | | using ArturRios.Heimdall.Data.Seeding; |
| | | 4 | | using ArturRios.Heimdall.Domain.Persistence; |
| | | 5 | | using Microsoft.AspNetCore.DataProtection; |
| | | 6 | | using Microsoft.Extensions.DependencyInjection; |
| | | 7 | | |
| | | 8 | | namespace ArturRios.Heimdall.Data.Registration; |
| | | 9 | | |
| | | 10 | | public static class DataServiceCollectionExtensions |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Registers the persistence services that spend single-use credentials, charge bounded budgets and |
| | | 14 | | /// take row locks as conditional writes. |
| | | 15 | | /// </summary> |
| | | 16 | | public static IServiceCollection AddPersistenceServices(this IServiceCollection services) |
| | | 17 | | { |
| | | 18 | | // Spending single-use credentials and charging bounded budgets as one conditional UPDATE |
| | | 19 | | // each, so concurrent requests cannot both spend one code or lose each other's counts. |
| | 973 | 20 | | services.AddScoped<IAtomicWrites, AtomicWrites>(); |
| | | 21 | | // UC-53's two-live-credentials limit, held under a row lock so concurrent issues cannot pass it. |
| | 973 | 22 | | services.AddScoped<IApplicationCredentialSlots, ApplicationCredentialSlots>(); |
| | | 23 | | // NFR-21 (#195): the tombstones a hard delete writes first, idempotently, as one upsert. |
| | 973 | 24 | | services.AddScoped<IErasedSubjects, ErasedSubjects>(); |
| | | 25 | | // FR-SR-14: the per-address budget of registration-attempt notices, charged as one upsert. |
| | 973 | 26 | | services.AddScoped<IAttemptNoticeBudgets, AttemptNoticeBudgets>(); |
| | | 27 | | // FR-SC-17: opening, answering, withdrawing and voiding scope owner invitations, each one |
| | | 28 | | // conditional write, and acceptance one transaction with the ownership it grants. |
| | 973 | 29 | | services.AddScoped<IScopeOwnerInvitations, ScopeOwnerInvitations>(); |
| | | 30 | | // #209, #212, #204: the transaction a command opens around the writes that must land together. |
| | 973 | 31 | | services.AddScoped<IWriteTransactions, WriteTransactions>(); |
| | | 32 | | // #204: a scope's delete cascaded to its members as set-based statements, never member by member. |
| | 973 | 33 | | services.AddScoped<IScopeCascades, ScopeCascades>(); |
| | | 34 | | // #202: the scope row locks that make a last-owner guard and its write one step. |
| | 973 | 35 | | services.AddScoped<IInvariantLocks, InvariantLocks>(); |
| | | 36 | | |
| | 973 | 37 | | return services; |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | /// <summary> |
| | | 41 | | /// Puts the Data Protection key ring in the database (UC-36, FR-2F-02). |
| | | 42 | | /// </summary> |
| | | 43 | | public static IServiceCollection AddDataProtectionKeyRing(this IServiceCollection services) |
| | | 44 | | { |
| | | 45 | | // UC-36 (FR-2F-02): the TOTP secret is encrypted at rest with ASP.NET Core's Data |
| | | 46 | | // Protection API before it is persisted. |
| | | 47 | | // |
| | | 48 | | // The key ring goes in the database, and the application name is fixed, because the default |
| | | 49 | | // is neither durable nor shared: keys land in a directory on the local filesystem that the |
| | | 50 | | // image does not persist and a second instance does not see. Either way the effect is the |
| | | 51 | | // same and it is silent — every stored TOTP secret becomes undecryptable, ITotpCodeVerifier |
| | | 52 | | // catches the CryptographicException and reports it as a wrong code, and every caller whose |
| | | 53 | | // second factor is an authenticator app is locked out of it with no indication why. That |
| | | 54 | | // survives a redeploy today only because a single container keeps its filesystem between |
| | | 55 | | // restarts; it would not survive being recreated, and it never survives a second replica |
| | | 56 | | // (NFR-06). |
| | 973 | 57 | | services.AddDataProtection() |
| | 973 | 58 | | .PersistKeysToDbContext<AppDbContext>() |
| | 973 | 59 | | .SetApplicationName("Heimdall"); |
| | | 60 | | |
| | 973 | 61 | | return services; |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// Registers the master user options and the database seeder. |
| | | 66 | | /// </summary> |
| | | 67 | | public static IServiceCollection AddDatabaseSeeding(this IServiceCollection services) |
| | | 68 | | { |
| | 973 | 69 | | services.AddSingleton(MasterUserOptions.FromEnvironment()); |
| | 973 | 70 | | services.AddScoped<DatabaseSeeder>(); |
| | | 71 | | |
| | 973 | 72 | | return services; |
| | | 73 | | } |
| | | 74 | | } |