Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ο»Ώusing Bit.Core.AdminConsole.Entities;
ο»Ώusing System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
Expand All @@ -7,6 +8,10 @@
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Microsoft.Extensions.Logging;

namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
Expand All @@ -19,6 +24,8 @@ public class OrganizationDeleteCommand : IOrganizationDeleteCommand
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISubscriberService _subscriberService;
private readonly IFeatureService _featureService;
private readonly ISendRepository _sendRepository;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly ILogger<OrganizationDeleteCommand> _logger;

public OrganizationDeleteCommand(
Expand All @@ -28,6 +35,8 @@ public OrganizationDeleteCommand(
ISsoConfigRepository ssoConfigRepository,
ISubscriberService subscriberService,
IFeatureService featureService,
ISendRepository sendRepository,
ISendFileStorageService sendFileStorageService,
ILogger<OrganizationDeleteCommand> logger)
{
_applicationCacheService = applicationCacheService;
Expand All @@ -36,6 +45,8 @@ public OrganizationDeleteCommand(
_ssoConfigRepository = ssoConfigRepository;
_subscriberService = subscriberService;
_featureService = featureService;
_sendRepository = sendRepository;
_sendFileStorageService = sendFileStorageService;
_logger = logger;
}

Expand Down Expand Up @@ -66,6 +77,8 @@ public async Task DeleteAsync(Organization organization)
}
}


await DeleteOrganizationOwnedSendFilesAsync(organization);
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
}
Expand All @@ -78,4 +91,19 @@ private async Task ValidateDeleteOrganizationAsync(Organization organization)
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}

// Only deletes Send Files
// Send objects are deleted via the call to _organizationRepository.DeleteAsync()
private async Task DeleteOrganizationOwnedSendFilesAsync(Organization organization)
{
var sends = await _sendRepository.GetManyByOrganizationIdAsync(organization.Id);
foreach (var send in sends.Where(s => s.Type == SendType.File))
{
var data = send.Data != null ? JsonSerializer.Deserialize<SendFileData>(send.Data) : null;
if (data?.Id != null)
{
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
}
}
}
}
29 changes: 28 additions & 1 deletion src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
Expand Down Expand Up @@ -69,6 +73,8 @@
private readonly IPricingClient _pricingClient;
private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
private readonly ISubscriberService _subscriberService;
private readonly ISendRepository _sendRepository;
private readonly ISendFileStorageService _sendFileStorageService;

public UserService(
IUserRepository userRepository,
Expand Down Expand Up @@ -102,7 +108,9 @@
IPolicyRequirementQuery policyRequirementQuery,
IPricingClient pricingClient,
IHasPremiumAccessQuery hasPremiumAccessQuery,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
ISendRepository sendRepository,
ISendFileStorageService sendFileStorageService)
: base(
store,
optionsAccessor,
Expand Down Expand Up @@ -141,6 +149,8 @@
_pricingClient = pricingClient;
_hasPremiumAccessQuery = hasPremiumAccessQuery;
_subscriberService = subscriberService;
_sendRepository = sendRepository;
_sendFileStorageService = sendFileStorageService;
}

public Guid? GetProperUserId(ClaimsPrincipal principal)
Expand Down Expand Up @@ -281,6 +291,8 @@
catch (BillingException) { }
}

await DeleteSendFilesForUserAsync(user);

await _userRepository.DeleteAsync(user);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
Expand Down Expand Up @@ -732,7 +744,7 @@
{
if (!CoreHelpers.FixedTimeEquals(
user.TwoFactorRecoveryCode,
recoveryCode.Replace(" ", string.Empty).Trim().ToLower()))

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (MsSqlMigratorUtility, ./util, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Identity, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Icons, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Billing, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (EventsProcessor, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Events, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 747 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Api, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)
{
return false;
}
Expand Down Expand Up @@ -1151,14 +1163,14 @@

public async Task<bool> ActiveNewDeviceVerificationException(Guid userId)
{
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());

Check warning on line 1166 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Icons, ./src, true)

The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call in 'UserService.ActiveNewDeviceVerificationException(Guid)' with a call to 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

Check warning on line 1166 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (EventsProcessor, ./src, true)

The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call in 'UserService.ActiveNewDeviceVerificationException(Guid)' with a call to 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

Check warning on line 1166 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Events, ./src, true)

The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call in 'UserService.ActiveNewDeviceVerificationException(Guid)' with a call to 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
var cacheValue = await _distributedCache.GetAsync(cacheKey);
return cacheValue != null;
}

public async Task ToggleNewDeviceVerificationException(Guid userId)
{
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());

Check warning on line 1173 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Icons, ./src, true)

The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call in 'UserService.ToggleNewDeviceVerificationException(Guid)' with a call to 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

Check warning on line 1173 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (EventsProcessor, ./src, true)

The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call in 'UserService.ToggleNewDeviceVerificationException(Guid)' with a call to 'string.Format(IFormatProvider, string, params object[])'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
var cacheValue = await _distributedCache.GetAsync(cacheKey);
if (cacheValue != null)
{
Expand Down Expand Up @@ -1186,4 +1198,19 @@
await _mailService.SendWelcomeEmailAsync(user);
}
}

// Only deletes Send Files
// Send objects are deleted via the call to _userRepository.DeleteAsync()
private async Task DeleteSendFilesForUserAsync(User user)
{
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
foreach (var send in sends.Where(s => s.Type == SendType.File))
{
var data = send.Data != null ? JsonSerializer.Deserialize<SendFileData>(send.Data) : null;
if (data?.Id != null)
{
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
}
}
}
}
12 changes: 12 additions & 0 deletions src/Core/Tools/Repositories/ISendRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ public interface ISendRepository : IRepository<Send, Guid>
/// </returns>
Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId);

/// <summary>
/// Loads all <see cref="Send"/>s owned by an organization.
/// </summary>
/// <param name="organizationId">
/// Identifies the organization.
/// </param>
/// <returns>
/// A task that completes once the <see cref="Send"/>s have been loaded.
/// The task's result contains the loaded <see cref="Send"/>s.
/// </returns>
Task<ICollection<Send>> GetManyByOrganizationIdAsync(Guid organizationId);

/// <summary>
/// Loads <see cref="Send"/>s scheduled for deletion.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,15 @@ public async Task UploadFileToExistingSendAsync(Stream stream, Send send)
}
public async Task DeleteSendAsync(Send send)
{
await _sendRepository.DeleteAsync(send);
if (send.Type == Enums.SendType.File)
if (send.Type == Enums.SendType.File && send.Data != null)
{
var data = JsonSerializer.Deserialize<SendFileData>(send.Data);
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
if (data?.Id != null)
{
await _sendFileStorageService.DeleteFileAsync(send, data.Id);
}
}
await _sendRepository.DeleteAsync(send);
await _pushNotificationService.PushSyncSendDeleteAsync(send);
}

Expand Down
16 changes: 16 additions & 0 deletions src/Infrastructure.Dapper/Tools/Repositories/SendRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ public async Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId)
}
}

/// <inheritdoc />
public async Task<ICollection<Send>> GetManyByOrganizationIdAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<Send>(
$"[{Schema}].[Send_ReadByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);

var sends = results.ToList();
UnprotectData(sends);
return sends;
}
}

/// <inheritdoc />
public async Task<ICollection<Send>> GetManyByDeletionDateAsync(DateTime deletionDateBefore)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ await dbContext.NotificationStatuses.Where(ns => ns.Notification.OrganizationId
await dbContext.Notifications.Where(n => n.OrganizationId == organization.Id)
.ExecuteDeleteAsync();

await dbContext.Sends.Where(s => s.OrganizationId == organization.Id)
.ExecuteDeleteAsync();

// The below section are 3 SPROCS in SQL Server but are only called by here
await dbContext.OrganizationApiKeys.Where(oa => oa.OrganizationId == organization.Id)
.ExecuteDeleteAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ public SendRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
}
}

/// <inheritdoc />
public async Task<ICollection<Core.Tools.Entities.Send>> GetManyByOrganizationIdAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.Sends.Where(s => s.OrganizationId == organizationId).ToListAsync();
return Mapper.Map<List<Core.Tools.Entities.Send>>(results);
}
}

/// <inheritdoc />
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId,
IEnumerable<Core.Tools.Entities.Send> sends)
Expand Down
7 changes: 7 additions & 0 deletions src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ BEGIN
WHERE
[OrganizationId] = @Id

-- Delete Organization Owned Sends
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ Differs in formatting, add blank line before comment

DELETE
FROM
[dbo].[Send]
WHERE
[OrganizationId] = @Id

DELETE
FROM
[dbo].[Organization]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[Send_ReadByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[SendView]
WHERE
[OrganizationId] = @OrganizationId
END
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ο»Ώusing Bit.Core.AdminConsole.Entities;
ο»Ώusing System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
Expand All @@ -10,6 +11,11 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
Expand Down Expand Up @@ -144,4 +150,52 @@ public async Task Delete_WhenFlagDisabled_HandlesBillingException(

await sutProvider.GetDependency<IOrganizationRepository>().Received(1).DeleteAsync(organization);
}

[Theory, PaidOrganizationCustomize, BitAutoData]
public async Task Delete_WithFileSends_DeletesFilesBeforeDbRecords(
Organization organization,
SutProvider<OrganizationDeleteCommand> sutProvider)
{
// Ensuring that the file is deleted first avoids the following situation:
// 1. DB row is deleted successfully
// 2. File blob fails to delete
// 3. File blob still exists but with no parent Send
var fileData = new SendFileData { Id = "file1", FileName = "test.txt", Size = 100 };
var fileSend = new Send
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = SendType.File,
Data = JsonSerializer.Serialize(fileData)
};
var textSend = new Send
{
Id = Guid.NewGuid(),
OrganizationId = organization.Id,
Type = SendType.Text,
Data = "{}"
};

sutProvider.GetDependency<ISendRepository>()
.GetManyByOrganizationIdAsync(organization.Id)
.Returns(new List<Send> { fileSend, textSend });

var callOrder = new List<string>();
sutProvider.GetDependency<ISendFileStorageService>()
.DeleteFileAsync(fileSend, fileData.Id)
.Returns(Task.CompletedTask)
.AndDoes(_ => callOrder.Add("file"));
sutProvider.GetDependency<IOrganizationRepository>()
.DeleteAsync(organization)
.Returns(Task.CompletedTask)
.AndDoes(_ => callOrder.Add("db"));

await sutProvider.Sut.DeleteAsync(organization);

await sutProvider.GetDependency<ISendFileStorageService>()
.Received(1).DeleteFileAsync(fileSend, fileData.Id);
await sutProvider.GetDependency<IOrganizationRepository>()
.Received(1).DeleteAsync(organization);
Assert.Equal(new[] { "file", "db" }, callOrder);
}
}
Loading
Loading