1
0
mirror of https://github.com/bitwarden/server synced 2025-12-14 23:33:41 +00:00

[PM-22104] Migrate default collection when org user is removed (#6135)

* migrate default collection to a shared collection when users are removed

* remove redundant logic

* fix test

* fix tests

* fix test

* clean up

* add migrations

* run dotnet format

* clean up, refactor duplicate logic to sproc, wip integration test

* fix sql

* add migration for new sproc

* integration test wip

* integration test wip

* integration test wip

* integration test wip

* fix integration test LINQ expression

* fix using wrong Id

* wip integration test for DeleteManyAsync

* fix LINQ

* only set DefaultUserEmail when it is null in sproc

* check for null

* spelling, separate create and update request models

* fix test

* fix child class

* refactor sproc

* clean up

* more cleanup

* fix tests

* fix user email

* remove unneccesary test

* add DefaultUserCollectionEmail to EF query

* fix test

* fix EF logic to match sprocs

* clean up logic

* cleanup
This commit is contained in:
Brandon Treston
2025-08-19 14:12:34 -04:00
committed by GitHub
parent 29d6288b27
commit c189e4aaf5
16 changed files with 1001 additions and 195 deletions

View File

@@ -57,8 +57,8 @@ public class OrganizationRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
});

View File

@@ -28,8 +28,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email, // TODO: EF does not enfore this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
@@ -37,6 +37,7 @@ public class OrganizationUserRepositoryTests
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user.Email
});
await organizationUserRepository.DeleteAsync(orgUser);
@@ -46,6 +47,171 @@ public class OrganizationUserRepositoryTests
Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_Migrates_UserDefaultCollection(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user1.Email
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user2.Email
});
var defaultUserCollection1 = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection 1",
Id = user1.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
var defaultUserCollection2 = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection 2",
Id = user2.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
// Create the CollectionUser entry for the defaultUserCollection
await collectionRepository.UpdateUsersAsync(defaultUserCollection1.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser1.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection2.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser2.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteManyAsync(new List<Guid> { orgUser1.Id, orgUser2.Id });
var newUser = await userRepository.GetByIdAsync(user1.Id);
Assert.NotNull(newUser);
Assert.NotEqual(newUser.AccountRevisionDate, user1.AccountRevisionDate);
var updatedCollection1 = await collectionRepository.GetByIdAsync(defaultUserCollection1.Id);
Assert.NotNull(updatedCollection1);
Assert.Equal(CollectionType.SharedCollection, updatedCollection1.Type);
Assert.Equal(user1.Email, updatedCollection1.DefaultUserCollectionEmail);
var updatedCollection2 = await collectionRepository.GetByIdAsync(defaultUserCollection2.Id);
Assert.NotNull(updatedCollection2);
Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type);
Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_Migrates_UserDefaultCollection(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user.Email
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
// Create the CollectionUser entry for the defaultUserCollection
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var newUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(newUser);
Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_Works(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -70,8 +236,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
@@ -79,6 +245,7 @@ public class OrganizationUserRepositoryTests
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user1.Email
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
@@ -86,6 +253,7 @@ public class OrganizationUserRepositoryTests
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user2.Email
});
await organizationUserRepository.DeleteManyAsync(new List<Guid>
@@ -135,8 +303,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
});
@@ -291,8 +459,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
});
@@ -354,6 +522,134 @@ public class OrganizationUserRepositoryTests
Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{id}@x-{domainName}", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user3 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{id}@{domainName}.example.com", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
UsePolicies = false,
UseSso = false,
UseKeyConnector = false,
UseScim = false,
UseGroups = false,
UseDirectory = false,
UseEvents = false,
UseTotp = false,
Use2fa = false,
UseApi = false,
UseResetPassword = false,
UseSecretsManager = false,
SelfHost = false,
UsersGetPremium = false,
UseCustomPermissions = false,
Enabled = true,
UsePasswordManager = false,
LimitCollectionCreation = false,
LimitCollectionDeletion = false,
LimitItemDeletion = false,
AllowAdminAccessToAllCollectionItems = false,
UseRiskInsights = false,
UseAdminSponsoredFamilies = false
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345",
};
organizationDomain.SetVerifiedDate();
organizationDomain.SetNextRunDate(12);
organizationDomain.SetJobRunCount();
await organizationDomainRepository.CreateAsync(organizationDomain);
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user3.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
Assert.NotNull(responseModel);
Assert.Single(responseModel);
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
IUserRepository userRepository,
@@ -369,7 +665,7 @@ public class OrganizationUserRepositoryTests
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUsers = users.Select(u => new OrganizationUser
@@ -403,7 +699,7 @@ public class OrganizationUserRepositoryTests
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUsers = users.Select(u => new OrganizationUser
@@ -435,8 +731,8 @@ public class OrganizationUserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = "billing@test.com", // TODO: EF does not enfore this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl,
BillingEmail = "billing@test.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL,
CreationDate = requestTime
});
@@ -862,119 +1158,6 @@ public class OrganizationUserRepositoryTests
Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var requestTime = DateTime.UtcNow;
var user1 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 1",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
CreationDate = requestTime,
RevisionDate = requestTime,
AccountRevisionDate = requestTime
});
var user2 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 2",
Email = $"test+{id}@x-{domainName}", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
CreationDate = requestTime,
RevisionDate = requestTime,
AccountRevisionDate = requestTime
});
var user3 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 3",
Email = $"test+{id}@{domainName}.example.com", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
CreationDate = requestTime,
RevisionDate = requestTime,
AccountRevisionDate = requestTime
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Id = CoreHelpers.GenerateComb(),
Name = $"Test Org {id}",
BillingEmail = user1.Email,
Plan = "Test",
Enabled = true,
CreationDate = requestTime,
RevisionDate = requestTime
});
var organizationDomain = new OrganizationDomain
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345",
CreationDate = requestTime
};
organizationDomain.SetNextRunDate(12);
organizationDomain.SetVerifiedDate();
organizationDomain.SetJobRunCount();
await organizationDomainRepository.CreateAsync(organizationDomain);
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
CreationDate = requestTime,
RevisionDate = requestTime
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
CreationDate = requestTime,
RevisionDate = requestTime
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user3.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
CreationDate = requestTime,
RevisionDate = requestTime
});
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
Assert.NotNull(responseModel);
Assert.Single(responseModel);
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
Assert.Equal(user1.Id, responseModel.Single().UserId);
Assert.Equal(organization.Id, responseModel.Single().OrganizationId);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty(
IUserRepository userRepository,
@@ -1039,6 +1222,120 @@ public class OrganizationUserRepositoryTests
Assert.Empty(responseModel);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email,
Plan = "Test",
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = null
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_WithEmptyEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email,
Plan = "Test",
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = "" // Empty string email
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess(
IUserRepository userRepository,

View File

@@ -55,7 +55,7 @@ public class UserRepositoryTests
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user3.Email, // TODO: EF does not enfore this being NOT NULL
BillingEmail = user3.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
});