From c05d904e85dfecce60c45fece815ccf4e62da9cc Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 15 Jan 2026 17:41:21 -0500 Subject: [PATCH] feat: Add initial DeleteEmergencyContactCommand --- .../Commands/DeleteEmergencyAccessCommand.cs | 63 +++++++++++++++++++ .../IDeleteEmergencyAccessCommand.cs | 28 +++++++++ .../UserServiceCollectionExtensions.cs | 8 +++ 3 files changed, 99 insertions(+) create mode 100644 src/Core/Auth/UserFeatures/EmergencyAccess/Commands/DeleteEmergencyAccessCommand.cs create mode 100644 src/Core/Auth/UserFeatures/EmergencyAccess/Interfaces/IDeleteEmergencyAccessCommand.cs diff --git a/src/Core/Auth/UserFeatures/EmergencyAccess/Commands/DeleteEmergencyAccessCommand.cs b/src/Core/Auth/UserFeatures/EmergencyAccess/Commands/DeleteEmergencyAccessCommand.cs new file mode 100644 index 0000000000..e7ae1ec14f --- /dev/null +++ b/src/Core/Auth/UserFeatures/EmergencyAccess/Commands/DeleteEmergencyAccessCommand.cs @@ -0,0 +1,63 @@ +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces; +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Mail.Mailer; +using Bit.Core.Repositories; + +namespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Commands; + +public class DeleteEmergencyAccessCommand( + IEmergencyAccessRepository _emergencyAccessRepository, + IMailer mailer) : IDeleteEmergencyAccessCommand +{ + /// + public async Task DeleteByIdGrantorIdAsync(Guid emergencyAccessId, Guid grantorId) + { + var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId); + + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorId) + { + throw new BadRequestException("Emergency Access not valid."); + } + + await _emergencyAccessRepository.DeleteAsync(emergencyAccess); + + // Send notification email to grantor + await SendEmailAsync(emergencyAccess.GrantorEmail, [emergencyAccess.GranteeName]); + return emergencyAccess; + } + + /// + public async Task> DeleteAllByGrantorIdAsync(Guid grantorId) + { + var emergencyAccessDetails = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(grantorId); + + foreach (var details in emergencyAccessDetails) + { + var emergencyAccess = details.ToEmergencyAccess(); + await _emergencyAccessRepository.DeleteAsync(emergencyAccess); + } + + // Send notification email to grantor + await SendEmailAsync( + emergencyAccessDetails.FirstOrDefault()?.GrantorEmail ?? string.Empty, + [.. emergencyAccessDetails.Select(e => e.GranteeName)]); + + return emergencyAccessDetails; + } + + private async Task SendEmailAsync(string grantorEmail, string[] granteeNames) + { + var email = new EmergencyAccessRemoveGranteesMail + { + ToEmails = [grantorEmail], + View = new EmergencyAccessRemoveGranteesMailView + { + RemovedGranteeNames = granteeNames + } + }; + + await mailer.SendEmail(email); + } +} diff --git a/src/Core/Auth/UserFeatures/EmergencyAccess/Interfaces/IDeleteEmergencyAccessCommand.cs b/src/Core/Auth/UserFeatures/EmergencyAccess/Interfaces/IDeleteEmergencyAccessCommand.cs new file mode 100644 index 0000000000..5c6838efab --- /dev/null +++ b/src/Core/Auth/UserFeatures/EmergencyAccess/Interfaces/IDeleteEmergencyAccessCommand.cs @@ -0,0 +1,28 @@ +using Bit.Core.Auth.Models.Data; +using Bit.Core.Exceptions; + +namespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces; + +/// +/// Command for deleting emergency access records based on the grantor's user ID. +/// +public interface IDeleteEmergencyAccessCommand +{ + /// + /// Deletes a single emergency access record for the specified grantor. + /// + /// The ID of the emergency access record to delete. + /// The ID of the grantor user who owns the emergency access record. + /// A task representing the asynchronous operation. + /// + /// Thrown when the emergency access record is not found or does not belong to the specified grantor. + /// + Task DeleteByIdGrantorIdAsync(Guid emergencyAccessId, Guid grantorId); + + /// + /// Deletes all emergency access records for the specified grantor. + /// + /// The ID of the grantor user whose emergency access records should be deleted. + /// A collection of the deleted emergency access records. + Task> DeleteAllByGrantorIdAsync(Guid grantorId); +} diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index 6249d1cb1c..356d5bf2bc 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using Bit.Core.Auth.Sso; using Bit.Core.Auth.UserFeatures.DeviceTrust; +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Commands; +using Bit.Core.Auth.UserFeatures.EmergencyAccess.Interfaces; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.Registration.Implementations; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; @@ -23,6 +25,7 @@ public static class UserServiceCollectionExtensions { services.AddScoped(); services.AddDeviceTrustCommands(); + services.AddEmergencyAccessCommands(); services.AddUserPasswordCommands(); services.AddUserRegistrationCommands(); services.AddWebAuthnLoginCommands(); @@ -36,6 +39,11 @@ public static class UserServiceCollectionExtensions services.AddScoped(); } + private static void AddEmergencyAccessCommands(this IServiceCollection services) + { + services.AddScoped(); + } + public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped();