1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-02 23:41:21 +01:00

Add DeleteManyAsync method and stored procedure

This commit is contained in:
Brandon 2024-11-11 16:50:16 -05:00
parent 702a81b161
commit 97b3be26f0
No known key found for this signature in database
GPG Key ID: A0E0EF0B207BA40D
5 changed files with 246 additions and 0 deletions

View File

@ -32,4 +32,5 @@ public interface IUserRepository : IRepository<User, Guid>
/// <param name="updateDataActions">Registered database calls to update re-encrypted data.</param>
Task UpdateUserKeyAndEncryptedDataAsync(User user,
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions);
Task DeleteManyAsync(IEnumerable<User> users);
}

View File

@ -172,6 +172,18 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
commandTimeout: 180);
}
}
public async Task DeleteManyAsync(IEnumerable<User> users)
{
var list = users.Select(user => user.Id);
using (var connection = new SqlConnection(ConnectionString))
{
await connection.ExecuteAsync(
$"[{Schema}].[{Table}_DeleteByIds]",
new { Ids = list.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure,
commandTimeout: 180);
}
}
public async Task UpdateStorageAsync(Guid id)
{

View File

@ -261,6 +261,54 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
var mappedUser = Mapper.Map<User>(user);
dbContext.Users.Remove(mappedUser);
await transaction.CommitAsync();
await dbContext.SaveChangesAsync();
}
}
public async Task DeleteManyAsync(IEnumerable<Core.Entities.User> users)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var transaction = await dbContext.Database.BeginTransactionAsync();
dbContext.WebAuthnCredentials.RemoveRange(dbContext.WebAuthnCredentials.Where(w => users.Any(u => u.Id == w.UserId)));
dbContext.Ciphers.RemoveRange(dbContext.Ciphers.Where(c => users.Any(u => u.Id == c.UserId)));
dbContext.Folders.RemoveRange(dbContext.Folders.Where(f => users.Any(u => u.Id == f.UserId)));
dbContext.AuthRequests.RemoveRange(dbContext.AuthRequests.Where(s => users.Any(u => u.Id == s.UserId)));
dbContext.Devices.RemoveRange(dbContext.Devices.Where(d => users.Any(u => u.Id == d.UserId)));
var collectionUsers = from cu in dbContext.CollectionUsers
join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id
where users.Any(u => u.Id == ou.UserId)
select cu;
dbContext.CollectionUsers.RemoveRange(collectionUsers);
var groupUsers = from gu in dbContext.GroupUsers
join ou in dbContext.OrganizationUsers on gu.OrganizationUserId equals ou.Id
where users.Any(u => u.Id == ou.UserId)
select gu;
dbContext.GroupUsers.RemoveRange(groupUsers);
dbContext.UserProjectAccessPolicy.RemoveRange(
dbContext.UserProjectAccessPolicy.Where(ap => users.Any(u => u.Id == ap.OrganizationUser.UserId)));
dbContext.UserServiceAccountAccessPolicy.RemoveRange(
dbContext.UserServiceAccountAccessPolicy.Where(ap => users.Any(u => u.Id == ap.OrganizationUser.UserId)));
dbContext.OrganizationUsers.RemoveRange(dbContext.OrganizationUsers.Where(ou => users.Any(u => u.Id == ou.UserId)));
dbContext.ProviderUsers.RemoveRange(dbContext.ProviderUsers.Where(pu => users.Any(u => u.Id == pu.UserId)));
dbContext.SsoUsers.RemoveRange(dbContext.SsoUsers.Where(su => users.Any(u => u.Id == su.UserId)));
dbContext.EmergencyAccesses.RemoveRange(
dbContext.EmergencyAccesses.Where(ea => users.Any(u => u.Id == ea.GrantorId || u.Id == ea.GranteeId)));
dbContext.Sends.RemoveRange(dbContext.Sends.Where(s => users.Any(u => u.Id == s.UserId)));
dbContext.NotificationStatuses.RemoveRange(dbContext.NotificationStatuses.Where(ns => users.Any(u => u.Id == ns.UserId)));
dbContext.Notifications.RemoveRange(dbContext.Notifications.Where(n => users.Any(u => u.Id == n.UserId)));
foreach (User u in users)
{
var mappedUser = Mapper.Map<User>(u);
dbContext.Users.Remove(mappedUser);
}
await transaction.CommitAsync();
await dbContext.SaveChangesAsync();
}

View File

@ -0,0 +1,144 @@
CREATE PROCEDURE [dbo].[User_DeleteByIds]
@Ids [dbo].[GuidIdArray]
WITH RECOMPILE
AS
BEGIN
SET NOCOUNT ON
DECLARE @BatchSize INT = 100
-- Delete ciphers
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION User_DeleteById_Ciphers
DELETE TOP(@BatchSize)
FROM
[dbo].[Cipher]
WHERE
[UserId] IN (@Ids)
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION User_DeleteById_Ciphers
END
BEGIN TRANSACTION User_DeleteById
-- Delete WebAuthnCredentials
DELETE
FROM
[dbo].[WebAuthnCredential]
WHERE
[UserId] IN (@Ids)
-- Delete folders
DELETE
FROM
[dbo].[Folder]
WHERE
[UserId] IN (@Ids)
-- Delete AuthRequest, must be before Device
DELETE
FROM
[dbo].[AuthRequest]
WHERE
[UserId] IN (@Ids)
-- Delete devices
DELETE
FROM
[dbo].[Device]
WHERE
[UserId] IN (@Ids)
-- Delete collection users
DELETE
CU
FROM
[dbo].[CollectionUser] CU
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]
WHERE
OU.[UserId] IN (@Ids)
-- Delete group users
DELETE
GU
FROM
[dbo].[GroupUser] GU
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]
WHERE
OU.[UserId] IN (@Ids)
-- Delete AccessPolicy
DELETE
AP
FROM
[dbo].[AccessPolicy] AP
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId]
WHERE
[UserId] IN (@Ids)
-- Delete organization users
DELETE
FROM
[dbo].[OrganizationUser]
WHERE
[UserId] IN (@Ids)
-- Delete provider users
DELETE
FROM
[dbo].[ProviderUser]
WHERE
[UserId] IN (@Ids)
-- Delete SSO Users
DELETE
FROM
[dbo].[SsoUser]
WHERE
[UserId] IN (@Ids)
-- Delete Emergency Accesses
DELETE
FROM
[dbo].[EmergencyAccess]
WHERE
[GrantorId] = @Id
OR
[GranteeId] = @Id
-- Delete Sends
DELETE
FROM
[dbo].[Send]
WHERE
[UserId] IN (@Ids)
-- Delete Notification Status
DELETE
FROM
[dbo].[NotificationStatus]
WHERE
[UserId] IN (@Ids)
-- Delete Notification
DELETE
FROM
[dbo].[Notification]
WHERE
[UserId] IN (@Ids)
-- Finally, delete the user
DELETE
FROM
[dbo].[User]
WHERE
[Id] = @Id
COMMIT TRANSACTION User_DeleteById
END

View File

@ -92,6 +92,47 @@ public class UserRepositoryTests
Assert.True(savedSqlUser == null);
}
[CiSkippedTheory, EfUserAutoData]
public async Task DeleteManyAsync_Works_DataMatches(IEnumerable<User> users, List<EfRepo.UserRepository> suts, SqlRepo.UserRepository sqlUserRepo)
{
foreach (var sut in suts)
{
foreach (var user in users)
{
var postEfUser = await sut.CreateAsync(user);
sut.ClearChangeTracking();
var savedEfUser = await sut.GetByIdAsync(postEfUser.Id);
Assert.True(savedEfUser != null);
sut.ClearChangeTracking();
await sut.DeleteAsync(savedEfUser);
sut.ClearChangeTracking();
savedEfUser = await sut.GetByIdAsync(savedEfUser.Id);
Assert.True(savedEfUser == null);
}
}
List<User> userList = new List<User>();
foreach (var user in users)
{
var postSqlUser = await sqlUserRepo.CreateAsync(user);
var savedSqlUser = await sqlUserRepo.GetByIdAsync(postSqlUser.Id);
Assert.True(savedSqlUser != null);
userList.Add(postSqlUser);
}
await sqlUserRepo.DeleteManyAsync(userList);
foreach (var user in userList)
{
var savedSqlUser = await sqlUserRepo.GetByIdAsync(user.Id);
Assert.True(savedSqlUser == null);
}
}
[CiSkippedTheory, EfUserAutoData]
public async Task GetByEmailAsync_Works_DataMatches(User user, UserCompare equalityComparer,
List<EfRepo.UserRepository> suts, SqlRepo.UserRepository sqlUserRepo)