mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +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:
@@ -146,7 +146,7 @@ public class CollectionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CollectionRequestModel model)
|
public async Task<CollectionResponseModel> Post(Guid orgId, [FromBody] CreateCollectionRequestModel model)
|
||||||
{
|
{
|
||||||
var collection = model.ToCollection(orgId);
|
var collection = model.ToCollection(orgId);
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ public class CollectionsController : Controller
|
|||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model)
|
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||||
{
|
{
|
||||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||||
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Bit.Core.Utilities;
|
|||||||
|
|
||||||
namespace Bit.Api.Models.Request;
|
namespace Bit.Api.Models.Request;
|
||||||
|
|
||||||
public class CollectionRequestModel
|
public class CreateCollectionRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[EncryptedString]
|
[EncryptedString]
|
||||||
@@ -40,7 +40,7 @@ public class CollectionBulkDeleteRequestModel
|
|||||||
public IEnumerable<Guid> Ids { get; set; }
|
public IEnumerable<Guid> Ids { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CollectionWithIdRequestModel : CollectionRequestModel
|
public class CollectionWithIdRequestModel : CreateCollectionRequestModel
|
||||||
{
|
{
|
||||||
public Guid? Id { get; set; }
|
public Guid? Id { get; set; }
|
||||||
|
|
||||||
@@ -50,3 +50,21 @@ public class CollectionWithIdRequestModel : CollectionRequestModel
|
|||||||
return base.ToCollection(existingCollection);
|
return base.ToCollection(existingCollection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UpdateCollectionRequestModel : CreateCollectionRequestModel
|
||||||
|
{
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
|
public new string Name { get; set; }
|
||||||
|
|
||||||
|
public override Collection ToCollection(Collection existingCollection)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(existingCollection.DefaultUserCollectionEmail) && !string.IsNullOrWhiteSpace(Name))
|
||||||
|
{
|
||||||
|
existingCollection.Name = Name;
|
||||||
|
}
|
||||||
|
existingCollection.ExternalId = ExternalId;
|
||||||
|
return existingCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public class CollectionDetailsResponseModel : CollectionResponseModel
|
|||||||
ReadOnly = collectionDetails.ReadOnly;
|
ReadOnly = collectionDetails.ReadOnly;
|
||||||
HidePasswords = collectionDetails.HidePasswords;
|
HidePasswords = collectionDetails.HidePasswords;
|
||||||
Manage = collectionDetails.Manage;
|
Manage = collectionDetails.Manage;
|
||||||
|
DefaultUserCollectionEmail = collectionDetails.DefaultUserCollectionEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ReadOnly { get; set; }
|
public bool ReadOnly { get; set; }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using AutoMapper;
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.KeyManagement.UserKey;
|
using Bit.Core.KeyManagement.UserKey;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
@@ -73,53 +74,91 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
return organizationUsers.Select(u => u.Id).ToList();
|
return organizationUsers.Select(u => u.Id).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task DeleteAsync(Core.Entities.OrganizationUser organizationUser) => await DeleteAsync(organizationUser.Id);
|
public override async Task DeleteAsync(Core.Entities.OrganizationUser organizationUser)
|
||||||
public async Task DeleteAsync(Guid organizationUserId)
|
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserId);
|
|
||||||
var orgUser = await dbContext.OrganizationUsers
|
var orgUser = await dbContext.OrganizationUsers
|
||||||
.Where(ou => ou.Id == organizationUserId)
|
.Where(ou => ou.Id == organizationUser.Id)
|
||||||
.FirstAsync();
|
.Select(ou => new
|
||||||
|
{
|
||||||
|
ou.Id,
|
||||||
|
ou.UserId,
|
||||||
|
OrgEmail = ou.Email,
|
||||||
|
UserEmail = ou.User.Email
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
var organizationId = orgUser?.OrganizationId;
|
if (orgUser == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException("User not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = !string.IsNullOrEmpty(orgUser.OrgEmail)
|
||||||
|
? orgUser.OrgEmail
|
||||||
|
: orgUser.UserEmail;
|
||||||
|
var organizationId = organizationUser?.OrganizationId;
|
||||||
var userId = orgUser?.UserId;
|
var userId = orgUser?.UserId;
|
||||||
|
var utcNow = DateTime.UtcNow;
|
||||||
|
|
||||||
if (orgUser?.OrganizationId != null && orgUser?.UserId != null)
|
using var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var ssoUsers = dbContext.SsoUsers
|
await dbContext.Collections
|
||||||
.Where(su => su.UserId == userId && su.OrganizationId == organizationId);
|
.Where(c => c.Type == CollectionType.DefaultUserCollection
|
||||||
dbContext.SsoUsers.RemoveRange(ssoUsers);
|
&& c.CollectionUsers.Any(cu => cu.OrganizationUserId == organizationUser.Id))
|
||||||
|
.ExecuteUpdateAsync(setters => setters
|
||||||
|
.SetProperty(c => c.Type, CollectionType.SharedCollection)
|
||||||
|
.SetProperty(c => c.RevisionDate, utcNow)
|
||||||
|
.SetProperty(c => c.DefaultUserCollectionEmail,
|
||||||
|
c => c.DefaultUserCollectionEmail == null ? email : c.DefaultUserCollectionEmail));
|
||||||
|
|
||||||
|
await dbContext.CollectionUsers
|
||||||
|
.Where(cu => cu.OrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.GroupUsers
|
||||||
|
.Where(gu => gu.OrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.SsoUsers
|
||||||
|
.Where(su => su.UserId == userId && su.OrganizationId == organizationId)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserProjectAccessPolicy
|
||||||
|
.Where(ap => ap.OrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserServiceAccountAccessPolicy
|
||||||
|
.Where(ap => ap.OrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserSecretAccessPolicy
|
||||||
|
.Where(ap => ap.OrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.OrganizationSponsorships
|
||||||
|
.Where(os => os.SponsoringOrganizationUserId == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.Users
|
||||||
|
.Where(u => u.Id == orgUser.UserId)
|
||||||
|
.ExecuteUpdateAsync(setters => setters
|
||||||
|
.SetProperty(u => u.AccountRevisionDate, utcNow));
|
||||||
|
|
||||||
|
await dbContext.OrganizationUsers
|
||||||
|
.Where(ou => ou.Id == organizationUser.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await transaction.CommitAsync();
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
var collectionUsers = dbContext.CollectionUsers
|
|
||||||
.Where(cu => cu.OrganizationUserId == organizationUserId);
|
|
||||||
dbContext.CollectionUsers.RemoveRange(collectionUsers);
|
|
||||||
|
|
||||||
var groupUsers = dbContext.GroupUsers
|
|
||||||
.Where(gu => gu.OrganizationUserId == organizationUserId);
|
|
||||||
dbContext.GroupUsers.RemoveRange(groupUsers);
|
|
||||||
|
|
||||||
dbContext.UserProjectAccessPolicy.RemoveRange(
|
|
||||||
dbContext.UserProjectAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId));
|
|
||||||
dbContext.UserServiceAccountAccessPolicy.RemoveRange(
|
|
||||||
dbContext.UserServiceAccountAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId));
|
|
||||||
dbContext.UserSecretAccessPolicy.RemoveRange(
|
|
||||||
dbContext.UserSecretAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId));
|
|
||||||
|
|
||||||
var orgSponsorships = await dbContext.OrganizationSponsorships
|
|
||||||
.Where(os => os.SponsoringOrganizationUserId == organizationUserId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
foreach (var orgSponsorship in orgSponsorships)
|
|
||||||
{
|
{
|
||||||
orgSponsorship.ToDelete = true;
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.OrganizationUsers.Remove(orgUser);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,31 +169,92 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var transaction = await dbContext.Database.BeginTransactionAsync();
|
var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(targetOrganizationUserIds);
|
|
||||||
|
|
||||||
await dbContext.CollectionUsers
|
try
|
||||||
.Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId))
|
{
|
||||||
.ExecuteDeleteAsync();
|
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(targetOrganizationUserIds);
|
||||||
|
|
||||||
await dbContext.GroupUsers
|
var organizationUsersToDelete = await dbContext.OrganizationUsers
|
||||||
.Where(gu => targetOrganizationUserIds.Contains(gu.OrganizationUserId))
|
.Where(ou => targetOrganizationUserIds.Contains(ou.Id))
|
||||||
.ExecuteDeleteAsync();
|
.Include(ou => ou.User)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
await dbContext.UserProjectAccessPolicy
|
var collectionUsers = await dbContext.CollectionUsers
|
||||||
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
.Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId))
|
||||||
.ExecuteDeleteAsync();
|
.ToListAsync();
|
||||||
await dbContext.UserServiceAccountAccessPolicy
|
|
||||||
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
await dbContext.UserSecretAccessPolicy
|
|
||||||
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
await dbContext.OrganizationUsers
|
var collectionIds = collectionUsers.Select(cu => cu.CollectionId).Distinct().ToList();
|
||||||
.Where(ou => targetOrganizationUserIds.Contains(ou.Id)).ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
var collections = await dbContext.Collections
|
||||||
await transaction.CommitAsync();
|
.Where(c => collectionIds.Contains(c.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var collectionsToUpdate = collections
|
||||||
|
.Where(c => c.Type == CollectionType.DefaultUserCollection)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var collectionUserLookup = collectionUsers.ToLookup(cu => cu.CollectionId);
|
||||||
|
|
||||||
|
foreach (var collection in collectionsToUpdate)
|
||||||
|
{
|
||||||
|
var collectionUser = collectionUserLookup[collection.Id].FirstOrDefault();
|
||||||
|
if (collectionUser != null)
|
||||||
|
{
|
||||||
|
var orgUser = organizationUsersToDelete.FirstOrDefault(ou => ou.Id == collectionUser.OrganizationUserId);
|
||||||
|
|
||||||
|
if (orgUser?.User != null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(collection.DefaultUserCollectionEmail))
|
||||||
|
{
|
||||||
|
var emailToUse = !string.IsNullOrEmpty(orgUser.Email)
|
||||||
|
? orgUser.Email
|
||||||
|
: orgUser.User.Email;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(emailToUse))
|
||||||
|
{
|
||||||
|
collection.DefaultUserCollectionEmail = emailToUse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collection.Type = CollectionType.SharedCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.CollectionUsers
|
||||||
|
.Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.GroupUsers
|
||||||
|
.Where(gu => targetOrganizationUserIds.Contains(gu.OrganizationUserId))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserProjectAccessPolicy
|
||||||
|
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserServiceAccountAccessPolicy
|
||||||
|
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.UserSecretAccessPolicy
|
||||||
|
.Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.OrganizationSponsorships
|
||||||
|
.Where(os => targetOrganizationUserIds.Contains(os.SponsoringOrganizationUserId))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.OrganizationUsers
|
||||||
|
.Where(ou => targetOrganizationUserIds.Contains(ou.Id)).ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Tuple<Core.Entities.OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)
|
public async Task<Tuple<Core.Entities.OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id)
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
c.CreationDate,
|
c.CreationDate,
|
||||||
c.RevisionDate,
|
c.RevisionDate,
|
||||||
c.ExternalId,
|
c.ExternalId,
|
||||||
c.Unmanaged
|
c.Unmanaged,
|
||||||
|
c.DefaultUserCollectionEmail
|
||||||
}).Select(collectionGroup => new CollectionAdminDetails
|
}).Select(collectionGroup => new CollectionAdminDetails
|
||||||
{
|
{
|
||||||
Id = collectionGroup.Key.Id,
|
Id = collectionGroup.Key.Id,
|
||||||
@@ -339,7 +340,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
Unmanaged = collectionGroup.Key.Unmanaged
|
Unmanaged = collectionGroup.Key.Unmanaged,
|
||||||
|
DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -353,7 +355,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
c.CreationDate,
|
c.CreationDate,
|
||||||
c.RevisionDate,
|
c.RevisionDate,
|
||||||
c.ExternalId,
|
c.ExternalId,
|
||||||
c.Unmanaged
|
c.Unmanaged,
|
||||||
|
c.DefaultUserCollectionEmail
|
||||||
}
|
}
|
||||||
into collectionGroup
|
into collectionGroup
|
||||||
select new CollectionAdminDetails
|
select new CollectionAdminDetails
|
||||||
@@ -369,7 +372,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
|||||||
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||||
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))),
|
||||||
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))),
|
||||||
Unmanaged = collectionGroup.Key.Unmanaged
|
Unmanaged = collectionGroup.Key.Unmanaged,
|
||||||
|
DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public class CollectionAdminDetailsQuery : IQuery<CollectionAdminDetails>
|
|||||||
ExternalId = x.c.ExternalId,
|
ExternalId = x.c.ExternalId,
|
||||||
CreationDate = x.c.CreationDate,
|
CreationDate = x.c.CreationDate,
|
||||||
RevisionDate = x.c.RevisionDate,
|
RevisionDate = x.c.RevisionDate,
|
||||||
|
DefaultUserCollectionEmail = x.c.DefaultUserCollectionEmail,
|
||||||
ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false,
|
ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false,
|
||||||
HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,
|
HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false,
|
||||||
Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,
|
Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById]
|
CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById]
|
||||||
@Id UNIQUEIDENTIFIER
|
@Id UNIQUEIDENTIFIER
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -17,6 +17,11 @@ BEGIN
|
|||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
|
|
||||||
|
-- Migrate DefaultUserCollection to SharedCollection
|
||||||
|
DECLARE @Ids [dbo].[GuidIdArray]
|
||||||
|
INSERT INTO @Ids (Id) VALUES (@Id)
|
||||||
|
EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids
|
||||||
|
|
||||||
IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL
|
IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId
|
EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ BEGIN
|
|||||||
|
|
||||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids
|
||||||
|
|
||||||
|
-- Migrate DefaultCollection to SharedCollection
|
||||||
|
EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids
|
||||||
|
|
||||||
DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray]
|
DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray]
|
||||||
|
|
||||||
INSERT INTO @UserAndOrganizationIds
|
INSERT INTO @UserAndOrganizationIds
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_MigrateDefaultCollection]
|
||||||
|
@Ids [dbo].[GuidIdArray] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET
|
||||||
|
[DefaultUserCollectionEmail] = CASE WHEN c.[DefaultUserCollectionEmail] IS NULL THEN u.[Email] ELSE c.[DefaultUserCollectionEmail] END,
|
||||||
|
[RevisionDate] = @UtcNow,
|
||||||
|
[Type] = 0
|
||||||
|
FROM
|
||||||
|
[dbo].[Collection] c
|
||||||
|
INNER JOIN [dbo].[CollectionUser] cu ON c.[Id] = cu.[CollectionId]
|
||||||
|
INNER JOIN [dbo].[OrganizationUser] ou ON cu.[OrganizationUserId] = ou.[Id]
|
||||||
|
INNER JOIN [dbo].[User] u ON ou.[UserId] = u.[Id]
|
||||||
|
INNER JOIN @Ids i ON ou.[Id] = i.[Id]
|
||||||
|
WHERE
|
||||||
|
c.[Type] = 1
|
||||||
|
END
|
||||||
|
GO
|
||||||
@@ -22,7 +22,7 @@ namespace Bit.Api.Test.Controllers;
|
|||||||
public class CollectionsControllerTests
|
public class CollectionsControllerTests
|
||||||
{
|
{
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_Success(Organization organization, CollectionRequestModel collectionRequest,
|
public async Task Post_Success(Organization organization, CreateCollectionRequestModel collectionRequest,
|
||||||
SutProvider<CollectionsController> sutProvider)
|
SutProvider<CollectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
Collection ExpectedCollection() => Arg.Is<Collection>(c =>
|
Collection ExpectedCollection() => Arg.Is<Collection>(c =>
|
||||||
@@ -46,9 +46,10 @@ public class CollectionsControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest,
|
public async Task Put_Success(Collection collection, UpdateCollectionRequestModel collectionRequest,
|
||||||
SutProvider<CollectionsController> sutProvider)
|
SutProvider<CollectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
|
collection.DefaultUserCollectionEmail = null;
|
||||||
Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&
|
Collection ExpectedCollection() => Arg.Is<Collection>(c => c.Id == collection.Id &&
|
||||||
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
|
c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId &&
|
||||||
c.OrganizationId == collection.OrganizationId);
|
c.OrganizationId == collection.OrganizationId);
|
||||||
@@ -72,7 +73,7 @@ public class CollectionsControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest,
|
public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, UpdateCollectionRequestModel collectionRequest,
|
||||||
SutProvider<CollectionsController> sutProvider)
|
SutProvider<CollectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IAuthorizationService>()
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
@@ -484,4 +485,176 @@ public class CollectionsControllerTests
|
|||||||
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
|
await sutProvider.GetDependency<IBulkAddCollectionAccessCommand>().DidNotReceiveWithAnyArgs()
|
||||||
.AddAccessAsync(default, default, default);
|
.AddAccessAsync(default, default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_With_NonNullName_DoesNotPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
|
||||||
|
SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var newName = "new name";
|
||||||
|
var originalName = "original name";
|
||||||
|
|
||||||
|
existingCollection.Name = originalName;
|
||||||
|
existingCollection.DefaultUserCollectionEmail = null;
|
||||||
|
|
||||||
|
collectionRequest.Name = newName;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetByIdAsync(existingCollection.Id)
|
||||||
|
.Returns(existingCollection);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||||
|
existingCollection,
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUpdateCollectionCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == newName),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_WithNullName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
|
||||||
|
SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalName = "original name";
|
||||||
|
|
||||||
|
existingCollection.Name = originalName;
|
||||||
|
existingCollection.DefaultUserCollectionEmail = null;
|
||||||
|
|
||||||
|
collectionRequest.Name = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetByIdAsync(existingCollection.Id)
|
||||||
|
.Returns(existingCollection);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||||
|
existingCollection,
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUpdateCollectionCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_WithDefaultUserCollectionEmail_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
|
||||||
|
SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalName = "original name";
|
||||||
|
var defaultUserCollectionEmail = "user@email.com";
|
||||||
|
|
||||||
|
existingCollection.Name = originalName;
|
||||||
|
existingCollection.DefaultUserCollectionEmail = defaultUserCollectionEmail;
|
||||||
|
|
||||||
|
collectionRequest.Name = "new name";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetByIdAsync(existingCollection.Id)
|
||||||
|
.Returns(existingCollection);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||||
|
existingCollection,
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUpdateCollectionCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName && c.DefaultUserCollectionEmail == defaultUserCollectionEmail),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_WithEmptyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
|
||||||
|
SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalName = "original name";
|
||||||
|
|
||||||
|
existingCollection.Name = originalName;
|
||||||
|
existingCollection.DefaultUserCollectionEmail = null;
|
||||||
|
|
||||||
|
collectionRequest.Name = ""; // Empty string
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetByIdAsync(existingCollection.Id)
|
||||||
|
.Returns(existingCollection);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||||
|
existingCollection,
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUpdateCollectionCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_WithWhitespaceOnlyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest,
|
||||||
|
SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var originalName = "original name";
|
||||||
|
|
||||||
|
existingCollection.Name = originalName;
|
||||||
|
existingCollection.DefaultUserCollectionEmail = null;
|
||||||
|
|
||||||
|
collectionRequest.Name = " "; // Whitespace only
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetByIdAsync(existingCollection.Id)
|
||||||
|
.Returns(existingCollection);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(),
|
||||||
|
existingCollection,
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(r => r.Contains(BulkCollectionOperations.Update)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IUpdateCollectionCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Id == existingCollection.Id && c.Name == originalName),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||||
|
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ public class OrganizationRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = $"Test Org {id}",
|
Name = $"Test Org {id}",
|
||||||
BillingEmail = user1.Email, // 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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
|
||||||
PrivateKey = "privatekey",
|
PrivateKey = "privatekey",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
Name = "Test Org",
|
||||||
BillingEmail = user.Email, // TODO: EF does not enfore 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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
|
||||||
});
|
});
|
||||||
|
|
||||||
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
@@ -37,6 +37,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
Email = user.Email
|
||||||
});
|
});
|
||||||
|
|
||||||
await organizationUserRepository.DeleteAsync(orgUser);
|
await organizationUserRepository.DeleteAsync(orgUser);
|
||||||
@@ -46,6 +47,171 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate);
|
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]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task DeleteManyAsync_Works(IUserRepository userRepository,
|
public async Task DeleteManyAsync_Works(IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@@ -70,8 +236,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
Name = "Test Org",
|
||||||
BillingEmail = user1.Email, // 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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
|
||||||
});
|
});
|
||||||
|
|
||||||
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
@@ -79,6 +245,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
UserId = user1.Id,
|
UserId = user1.Id,
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
Email = user1.Email
|
||||||
});
|
});
|
||||||
|
|
||||||
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
@@ -86,6 +253,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
UserId = user2.Id,
|
UserId = user2.Id,
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
Email = user2.Email
|
||||||
});
|
});
|
||||||
|
|
||||||
await organizationUserRepository.DeleteManyAsync(new List<Guid>
|
await organizationUserRepository.DeleteManyAsync(new List<Guid>
|
||||||
@@ -135,8 +303,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
Name = "Test Org",
|
||||||
BillingEmail = user1.Email, // 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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
|
||||||
PrivateKey = "privatekey",
|
PrivateKey = "privatekey",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,8 +459,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
Name = "Test Org",
|
||||||
BillingEmail = user1.Email, // 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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
|
||||||
PrivateKey = "privatekey",
|
PrivateKey = "privatekey",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,6 +522,134 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);
|
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]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
|
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@@ -369,7 +665,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
{
|
{
|
||||||
Name = $"test-{Guid.NewGuid()}",
|
Name = $"test-{Guid.NewGuid()}",
|
||||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
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
|
var orgUsers = users.Select(u => new OrganizationUser
|
||||||
@@ -403,7 +699,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
{
|
{
|
||||||
Name = $"test-{Guid.NewGuid()}",
|
Name = $"test-{Guid.NewGuid()}",
|
||||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
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
|
var orgUsers = users.Select(u => new OrganizationUser
|
||||||
@@ -435,8 +731,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
Name = "Test Org",
|
||||||
BillingEmail = "billing@test.com", // TODO: EF does not enfore 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,
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULL,
|
||||||
CreationDate = requestTime
|
CreationDate = requestTime
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -862,119 +1158,6 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id);
|
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]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty(
|
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@@ -1039,6 +1222,120 @@ public class OrganizationUserRepositoryTests
|
|||||||
Assert.Empty(responseModel);
|
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]
|
[DatabaseTheory, DatabaseData]
|
||||||
public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess(
|
public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ public class UserRepositoryTests
|
|||||||
var organization = await organizationRepository.CreateAsync(new Organization
|
var organization = await organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = "Test Org",
|
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
|
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_MigrateDefaultCollection]
|
||||||
|
@Ids [dbo].[GuidIdArray] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
|
||||||
|
|
||||||
|
UPDATE c
|
||||||
|
SET
|
||||||
|
[DefaultUserCollectionEmail] = CASE WHEN c.[DefaultUserCollectionEmail] IS NULL THEN u.[Email] ELSE c.[DefaultUserCollectionEmail] END,
|
||||||
|
[RevisionDate] = @UtcNow,
|
||||||
|
[Type] = 0
|
||||||
|
FROM
|
||||||
|
[dbo].[Collection] c
|
||||||
|
INNER JOIN [dbo].[CollectionUser] cu ON c.[Id] = cu.[CollectionId]
|
||||||
|
INNER JOIN [dbo].[OrganizationUser] ou ON cu.[OrganizationUserId] = ou.[Id]
|
||||||
|
INNER JOIN [dbo].[User] u ON ou.[UserId] = u.[Id]
|
||||||
|
INNER JOIN @Ids i ON ou.[Id] = i.[Id]
|
||||||
|
WHERE
|
||||||
|
c.[Type] = 1
|
||||||
|
END
|
||||||
|
GO
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id
|
||||||
|
|
||||||
|
DECLARE @OrganizationId UNIQUEIDENTIFIER
|
||||||
|
DECLARE @UserId UNIQUEIDENTIFIER
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
@OrganizationId = [OrganizationId],
|
||||||
|
@UserId = [UserId]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
|
||||||
|
-- Migrate DefaultUserCollection to SharedCollection
|
||||||
|
DECLARE @Ids [dbo].[GuidIdArray]
|
||||||
|
INSERT INTO @Ids (Id) VALUES (@Id)
|
||||||
|
EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids
|
||||||
|
|
||||||
|
IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId
|
||||||
|
END
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionUser]
|
||||||
|
WHERE
|
||||||
|
[OrganizationUserId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[GroupUser]
|
||||||
|
WHERE
|
||||||
|
[OrganizationUserId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[AccessPolicy]
|
||||||
|
WHERE
|
||||||
|
[OrganizationUserId] = @Id
|
||||||
|
|
||||||
|
EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
105
util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql
Normal file
105
util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_DeleteByIds]
|
||||||
|
@Ids [dbo].[GuidIdArray] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids
|
||||||
|
|
||||||
|
-- Migrate DefaultCollection to SharedCollection
|
||||||
|
EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids
|
||||||
|
|
||||||
|
DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray]
|
||||||
|
|
||||||
|
INSERT INTO @UserAndOrganizationIds
|
||||||
|
(Id1, Id2)
|
||||||
|
SELECT
|
||||||
|
UserId,
|
||||||
|
OrganizationId
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser] OU
|
||||||
|
INNER JOIN
|
||||||
|
@Ids OUIds ON OUIds.Id = OU.Id
|
||||||
|
WHERE
|
||||||
|
UserId IS NOT NULL AND
|
||||||
|
OrganizationId IS NOT NULL
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[SsoUser_DeleteMany] @UserAndOrganizationIds
|
||||||
|
END
|
||||||
|
|
||||||
|
DECLARE @BatchSize INT = 100
|
||||||
|
|
||||||
|
-- Delete CollectionUsers
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION CollectionUser_DeleteMany_CUs
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize) CU
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionUser] CU
|
||||||
|
INNER JOIN
|
||||||
|
@Ids I ON I.Id = CU.OrganizationUserId
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION CollectionUser_DeleteMany_CUs
|
||||||
|
END
|
||||||
|
|
||||||
|
SET @BatchSize = 100;
|
||||||
|
|
||||||
|
-- Delete GroupUsers
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION GroupUser_DeleteMany_GroupUsers
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize) GU
|
||||||
|
FROM
|
||||||
|
[dbo].[GroupUser] GU
|
||||||
|
INNER JOIN
|
||||||
|
@Ids I ON I.Id = GU.OrganizationUserId
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION GroupUser_DeleteMany_GroupUsers
|
||||||
|
END
|
||||||
|
|
||||||
|
SET @BatchSize = 100;
|
||||||
|
|
||||||
|
-- Delete User Access Policies
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION AccessPolicy_DeleteMany_Users
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize) AP
|
||||||
|
FROM
|
||||||
|
[dbo].[AccessPolicy] AP
|
||||||
|
INNER JOIN
|
||||||
|
@Ids I ON I.Id = AP.OrganizationUserId
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION AccessPolicy_DeleteMany_Users
|
||||||
|
END
|
||||||
|
|
||||||
|
EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids
|
||||||
|
|
||||||
|
SET @BatchSize = 100;
|
||||||
|
|
||||||
|
-- Delete OrganizationUsers
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION OrganizationUser_DeleteMany_OUs
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize) OU
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser] OU
|
||||||
|
INNER JOIN
|
||||||
|
@Ids I ON I.Id = OU.Id
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION OrganizationUser_DeleteMany_OUs
|
||||||
|
END
|
||||||
|
END
|
||||||
|
GO
|
||||||
Reference in New Issue
Block a user