From 4264fc07294fe27d1d69f22ac7f89d665d448377 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 22 May 2024 12:59:19 -0400 Subject: [PATCH] [PM-7004] Org Admin Initiate Delete (#3905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * org delete * move org id to URL path * tweaks * lint fixes * Update src/Core/Services/Implementations/HandlebarsMailService.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Update src/Core/Services/Implementations/HandlebarsMailService.cs Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * PR feedback * fix id * [PM-7004] Move OrgDeleteTokenable to AdminConsole ownership * [PM-7004] Add consolidated billing logic into organization delete request acceptance endpoint * [PM-7004] Delete unused IOrganizationService.DeleteAsync(Organization organization, string token) method * [PM-7004] Fix unit tests * [PM-7004] Update delete organization request email templates * Add success message when initiating organization deletion * Refactor OrganizationsController request delete initiation action to handle exceptions --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Rui Tome --- .../Controllers/OrganizationsController.cs | 29 ++++++++ .../Models/OrganizationInitiateDeleteModel.cs | 12 +++ .../Views/Organizations/Edit.cshtml | 73 ++++++++++++------- src/Admin/Views/Shared/_Layout.cshtml | 8 ++ .../Controllers/OrganizationsController.cs | 38 +++++++++- ...nizationVerifyDeleteRecoverRequestModel.cs | 9 +++ .../Business/Tokenables/OrgDeleteTokenable.cs | 32 ++++++++ .../Services/IOrganizationService.cs | 1 + .../Implementations/OrganizationService.cs | 21 ++++++ .../InitiateDeleteOrganzation.html.hbs | 39 ++++++++++ .../InitiateDeleteOrganzation.text.hbs | 17 +++++ .../Mail/OrganizationInitiateDeleteModel.cs | 23 ++++++ src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 24 ++++++ .../NoopImplementations/NoopMailService.cs | 5 ++ src/Core/Utilities/ModelStateExtensions.cs | 16 ++++ .../Utilities/ServiceCollectionExtensions.cs | 7 ++ .../OrganizationsControllerTests.cs | 7 +- 18 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs create mode 100644 src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs create mode 100644 src/Core/AdminConsole/Models/Business/Tokenables/OrgDeleteTokenable.cs create mode 100644 src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.text.hbs create mode 100644 src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs create mode 100644 src/Core/Utilities/ModelStateExtensions.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 19b450686..88c046746 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -269,6 +269,35 @@ public class OrganizationsController : Controller return RedirectToAction("Index"); } + [HttpPost] + [ValidateAntiForgeryToken] + [RequirePermission(Permission.Org_Delete)] + public async Task DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model) + { + if (!ModelState.IsValid) + { + TempData["Error"] = ModelState.GetErrorMessage(); + } + else + { + try + { + var organization = await _organizationRepository.GetByIdAsync(id); + if (organization != null) + { + await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail); + TempData["Success"] = "The request to initiate deletion of the organization has been sent."; + } + } + catch (Exception ex) + { + TempData["Error"] = ex.Message; + } + } + + return RedirectToAction("Edit", new { id }); + } + public async Task TriggerBillingSync(Guid id) { var organization = await _organizationRepository.GetByIdAsync(id); diff --git a/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs b/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs new file mode 100644 index 000000000..5e9055be5 --- /dev/null +++ b/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.AdminConsole.Models; + +public class OrganizationInitiateDeleteModel +{ + [Required] + [EmailAddress] + [StringLength(256)] + [Display(Name = "Admin Email")] + public string AdminEmail { get; set; } +} diff --git a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml index 23ba63bbe..ad64e6e4f 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Edit.cshtml @@ -1,4 +1,4 @@ -@using Bit.Admin.Enums; +@using Bit.Admin.Enums; @using Bit.Admin.Models @using Bit.Core.Enums @inject Bit.Admin.Services.IAccessControlService AccessControlService @@ -18,24 +18,43 @@ } + @if (TempData["Success"] != null) + { + + } @RenderSection("Scripts", required: false) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 917da4aaf..979c5d16d 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -11,6 +11,7 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; @@ -26,6 +27,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Tokens; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -54,6 +56,7 @@ public class OrganizationsController : Controller private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; private readonly IProviderRepository _providerRepository; private readonly IScaleSeatsCommand _scaleSeatsCommand; + private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -73,7 +76,8 @@ public class OrganizationsController : Controller IPushNotificationService pushNotificationService, IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand, IProviderRepository providerRepository, - IScaleSeatsCommand scaleSeatsCommand) + IScaleSeatsCommand scaleSeatsCommand, + IDataProtectorTokenFactory orgDeleteTokenDataFactory) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -93,6 +97,7 @@ public class OrganizationsController : Controller _organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand; _providerRepository = providerRepository; _scaleSeatsCommand = scaleSeatsCommand; + _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; } [HttpGet("{id}")] @@ -279,6 +284,37 @@ public class OrganizationsController : Controller await _organizationService.DeleteAsync(organization); } + [HttpPost("{id}/delete-recover-token")] + [AllowAnonymous] + public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model) + { + var organization = await _organizationRepository.GetByIdAsync(id); + if (organization == null) + { + throw new NotFoundException(); + } + + if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.IsValid(organization)) + { + throw new BadRequestException("Invalid token."); + } + + var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + if (consolidatedBillingEnabled && organization.IsValidClient()) + { + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + if (provider.IsBillable()) + { + await _scaleSeatsCommand.ScalePasswordManagerSeats( + provider, + organization.PlanType, + -organization.Seats ?? 0); + } + } + + await _organizationService.DeleteAsync(organization); + } + [HttpPost("{id}/import")] public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model) { diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs new file mode 100644 index 000000000..36dba6ed9 --- /dev/null +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.AdminConsole.Models.Request.Organizations; + +public class OrganizationVerifyDeleteRecoverRequestModel +{ + [Required] + public string Token { get; set; } +} diff --git a/src/Core/AdminConsole/Models/Business/Tokenables/OrgDeleteTokenable.cs b/src/Core/AdminConsole/Models/Business/Tokenables/OrgDeleteTokenable.cs new file mode 100644 index 000000000..6a769010a --- /dev/null +++ b/src/Core/AdminConsole/Models/Business/Tokenables/OrgDeleteTokenable.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.Models.Business.Tokenables; + +public class OrgDeleteTokenable : Tokens.ExpiringTokenable +{ + public const string ClearTextPrefix = ""; + public const string DataProtectorPurpose = "OrgDeleteDataProtector"; + public const string TokenIdentifier = "OrgDelete"; + public string Identifier { get; set; } = TokenIdentifier; + public Guid Id { get; set; } + + [JsonConstructor] + public OrgDeleteTokenable(DateTime expirationDate) + { + ExpirationDate = expirationDate; + } + + public OrgDeleteTokenable(Organization organization, int hoursTillExpiration) + { + Id = organization.Id; + ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration); + } + + public bool IsValid(Organization organization) + { + return Id == organization.Id; + } + + protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default; +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 86611bdd5..0d2472b95 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -34,6 +34,7 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); + Task InitiateDeleteAsync(Organization organization, string orgAdminEmail); Task DeleteAsync(Organization organization); Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 8bf86a8ee..f32b29d83 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -60,6 +61,7 @@ public class OrganizationService : IOrganizationService private readonly IProviderUserRepository _providerUserRepository; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; + private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly IProviderRepository _providerRepository; private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; @@ -94,6 +96,7 @@ public class OrganizationService : IOrganizationService IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, + IDataProtectorTokenFactory orgDeleteTokenDataFactory, IProviderRepository providerRepository, IFeatureService featureService) { @@ -123,6 +126,7 @@ public class OrganizationService : IOrganizationService _providerUserRepository = providerUserRepository; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; + _orgDeleteTokenDataFactory = orgDeleteTokenDataFactory; _providerRepository = providerRepository; _orgUserInviteTokenableFactory = orgUserInviteTokenableFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; @@ -811,6 +815,23 @@ public class OrganizationService : IOrganizationService } } + public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail) + { + var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail); + if (orgAdmin == null) + { + throw new BadRequestException("Org admin not found."); + } + var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id); + if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed || + (orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner)) + { + throw new BadRequestException("Org admin not found."); + } + var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1)); + await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token); + } + public async Task DeleteAsync(Organization organization) { await ValidateDeleteOrganizationAsync(organization); diff --git a/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.html.hbs b/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.html.hbs new file mode 100644 index 000000000..7118fcfef --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.html.hbs @@ -0,0 +1,39 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + + + + + + + +
+ We recently received your request to permanently delete the following Bitwarden organization: +
+ Name: {{OrganizationName}}
+ ID: {{OrganizationId}}
+ Created: {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}
+ Plan: {{OrganizationPlan}}
+ Number of seats: {{OrganizationSeats}}
+ Billing email address: {{OrganizationBillingEmail}} +
+ Click the link below to delete your Bitwarden organization. +
+ If you did not request this email to delete your Bitwarden organization, please contact us. +
+
+
+ + Delete Your Organization + +
+
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.text.hbs b/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.text.hbs new file mode 100644 index 000000000..097743838 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/InitiateDeleteOrganzation.text.hbs @@ -0,0 +1,17 @@ +{{#>BasicTextLayout}} +We recently received your request to permanently delete the following Bitwarden organization: + +- Name: {{OrganizationName}} +- ID: {{OrganizationId}} +- Created: {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}} +- Plan: {{OrganizationPlan}} +- Number of seats: {{OrganizationSeats}} +- Billing email address: {{OrganizationBillingEmail}} + +Click the link below to complete the deletion of your organization. + +If you did not request this email to delete your Bitwarden organization, please contact us. + +{{{Url}}} + +{{/BasicTextLayout}} diff --git a/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs b/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs new file mode 100644 index 000000000..4e13abf65 --- /dev/null +++ b/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.Mail; + +namespace Bit.Core.Auth.Models.Mail; + +public class OrganizationInitiateDeleteModel : BaseMailModel +{ + public string Url => string.Format("{0}/verify-recover-delete-org?orgId={1}&token={2}&name={3}", + WebVaultUrl, + OrganizationId, + Token, + OrganizationNameUrlEncoded); + + public string Token { get; set; } + public Guid OrganizationId { get; set; } + public string OrganizationName { get; set; } + public string OrganizationNameUrlEncoded { get; set; } + public string OrganizationPlan { get; set; } + public string OrganizationSeats { get; set; } + public string OrganizationBillingEmail { get; set; } + public string OrganizationCreationDate { get; set; } + public string OrganizationCreationTime { get; set; } + public string TimeZone { get; set; } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 7943eca95..4db8f14fd 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -79,5 +79,6 @@ public interface IMailService Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier); Task SendTrialInitiationEmailAsync(string email); Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token); + Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 496d0ea0c..7e8de10ce 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -1002,6 +1002,30 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token) + { + var message = CreateDefaultMessage("Request to Delete Your Organization", email); + var model = new OrganizationInitiateDeleteModel + { + Token = WebUtility.UrlEncode(token), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + OrganizationId = organization.Id, + OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false), + OrganizationNameUrlEncoded = WebUtility.UrlEncode(organization.Name), + OrganizationBillingEmail = organization.BillingEmail, + OrganizationPlan = organization.Plan, + OrganizationSeats = organization.Seats.ToString(), + OrganizationCreationDate = organization.CreationDate.ToLongDateString(), + OrganizationCreationTime = organization.CreationDate.ToShortTimeString(), + TimeZone = _utcTimeZoneDisplay, + }; + await AddMessageContentAsync(message, "InitiateDeleteOrganzation", model); + message.MetaData.Add("SendGridBypassListManagement", true); + message.Category = "InitiateDeleteOrganzation"; + await _mailDeliveryService.SendEmailAsync(message); + } + private static string GetUserIdentifier(string email, string userName) { return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index f86d40a19..198738e3d 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -270,5 +270,10 @@ public class NoopMailService : IMailService } public Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token) => throw new NotImplementedException(); + + public Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token) + { + return Task.FromResult(0); + } } diff --git a/src/Core/Utilities/ModelStateExtensions.cs b/src/Core/Utilities/ModelStateExtensions.cs new file mode 100644 index 000000000..dc4f95c15 --- /dev/null +++ b/src/Core/Utilities/ModelStateExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Bit.Core.Utilities; + +public static class ModelStateExtensions +{ + public static string GetErrorMessage(this ModelStateDictionary modelState) + { + var errors = modelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + + return string.Join("; ", errors); + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index bc3c68e87..66048f91a 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -150,6 +150,13 @@ public static class ServiceCollectionExtensions public static void AddTokenizers(this IServiceCollection services) { + services.AddSingleton>(serviceProvider => + new DataProtectorTokenFactory( + OrgDeleteTokenable.ClearTextPrefix, + OrgDeleteTokenable.DataProtectorPurpose, + serviceProvider.GetDataProtectionProvider(), + serviceProvider.GetRequiredService>>()) + ); services.AddSingleton>(serviceProvider => new DataProtectorTokenFactory( EmergencyAccessInviteTokenable.ClearTextPrefix, diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 31bcef0bd..a6844d8c2 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -5,6 +5,7 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -20,6 +21,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tokens; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using NSubstitute; using Xunit; @@ -47,6 +49,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; private readonly IProviderRepository _providerRepository; private readonly IScaleSeatsCommand _scaleSeatsCommand; + private readonly IDataProtectorTokenFactory _orgDeleteTokenDataFactory; private readonly OrganizationsController _sut; @@ -70,6 +73,7 @@ public class OrganizationsControllerTests : IDisposable _organizationEnableCollectionEnhancementsCommand = Substitute.For(); _providerRepository = Substitute.For(); _scaleSeatsCommand = Substitute.For(); + _orgDeleteTokenDataFactory = Substitute.For>(); _sut = new OrganizationsController( _organizationRepository, @@ -89,7 +93,8 @@ public class OrganizationsControllerTests : IDisposable _pushNotificationService, _organizationEnableCollectionEnhancementsCommand, _providerRepository, - _scaleSeatsCommand); + _scaleSeatsCommand, + _orgDeleteTokenDataFactory); } public void Dispose()