mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 13:38:13 -05:00
[AC-1116] Assign new imported collections to the importing user with Manage permission (#3424)
* [AC-1116] Assigning imported collections to the importing user with Manage permission * [AC-1116] Added unit tests
This commit is contained in:
parent
4e8284cf81
commit
e2d644f136
@ -32,7 +32,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
|
|||||||
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||||
IEnumerable<CollectionCipher> collectionCiphers);
|
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
|
||||||
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
||||||
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
|
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
|
||||||
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
|
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);
|
||||||
|
@ -27,6 +27,7 @@ public class CipherService : ICipherService
|
|||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
private readonly ICollectionCipherRepository _collectionCipherRepository;
|
||||||
private readonly IPushNotificationService _pushService;
|
private readonly IPushNotificationService _pushService;
|
||||||
private readonly IAttachmentStorageService _attachmentStorageService;
|
private readonly IAttachmentStorageService _attachmentStorageService;
|
||||||
@ -44,6 +45,7 @@ public class CipherService : ICipherService
|
|||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ICollectionCipherRepository collectionCipherRepository,
|
ICollectionCipherRepository collectionCipherRepository,
|
||||||
IPushNotificationService pushService,
|
IPushNotificationService pushService,
|
||||||
IAttachmentStorageService attachmentStorageService,
|
IAttachmentStorageService attachmentStorageService,
|
||||||
@ -59,6 +61,7 @@ public class CipherService : ICipherService
|
|||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_collectionCipherRepository = collectionCipherRepository;
|
_collectionCipherRepository = collectionCipherRepository;
|
||||||
_pushService = pushService;
|
_pushService = pushService;
|
||||||
_attachmentStorageService = attachmentStorageService;
|
_attachmentStorageService = attachmentStorageService;
|
||||||
@ -747,6 +750,7 @@ public class CipherService : ICipherService
|
|||||||
var org = collections.Count > 0 ?
|
var org = collections.Count > 0 ?
|
||||||
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
|
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
|
||||||
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
|
await _organizationRepository.GetByIdAsync(ciphers.FirstOrDefault(c => c.OrganizationId.HasValue).OrganizationId.Value);
|
||||||
|
var importingOrgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, importingUserId);
|
||||||
|
|
||||||
if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
|
if (collections.Count > 0 && org != null && org.MaxCollections.HasValue)
|
||||||
{
|
{
|
||||||
@ -764,18 +768,25 @@ public class CipherService : ICipherService
|
|||||||
cipher.SetNewId();
|
cipher.SetNewId();
|
||||||
}
|
}
|
||||||
|
|
||||||
var userCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
|
||||||
|
|
||||||
//Assign id to the ones that don't exist in DB
|
//Assign id to the ones that don't exist in DB
|
||||||
//Need to keep the list order to create the relationships
|
//Need to keep the list order to create the relationships
|
||||||
List<Collection> newCollections = new List<Collection>();
|
var newCollections = new List<Collection>();
|
||||||
|
var newCollectionUsers = new List<CollectionUser>();
|
||||||
|
|
||||||
foreach (var collection in collections)
|
foreach (var collection in collections)
|
||||||
{
|
{
|
||||||
if (!userCollectionsIds.Contains(collection.Id))
|
if (!organizationCollectionsIds.Contains(collection.Id))
|
||||||
{
|
{
|
||||||
collection.SetNewId();
|
collection.SetNewId();
|
||||||
newCollections.Add(collection);
|
newCollections.Add(collection);
|
||||||
|
newCollectionUsers.Add(new CollectionUser
|
||||||
|
{
|
||||||
|
CollectionId = collection.Id,
|
||||||
|
OrganizationUserId = importingOrgUser.Id,
|
||||||
|
Manage = true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -799,7 +810,7 @@ public class CipherService : ICipherService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create it all
|
// Create it all
|
||||||
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers);
|
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
|
||||||
|
|
||||||
// push
|
// push
|
||||||
await _pushService.PushSyncVaultAsync(importingUserId);
|
await _pushService.PushSyncVaultAsync(importingUserId);
|
||||||
|
@ -589,7 +589,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||||
IEnumerable<CollectionCipher> collectionCiphers)
|
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers)
|
||||||
{
|
{
|
||||||
if (!ciphers.Any())
|
if (!ciphers.Any())
|
||||||
{
|
{
|
||||||
@ -631,6 +631,16 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collectionUsers.Any())
|
||||||
|
{
|
||||||
|
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
||||||
|
{
|
||||||
|
bulkCopy.DestinationTableName = "[dbo].[CollectionUser]";
|
||||||
|
var dataTable = BuildCollectionUsersTable(bulkCopy, collectionUsers);
|
||||||
|
bulkCopy.WriteToServer(dataTable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
$"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]",
|
$"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]",
|
||||||
new { OrganizationId = ciphers.First().OrganizationId },
|
new { OrganizationId = ciphers.First().OrganizationId },
|
||||||
@ -896,6 +906,53 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
return collectionCiphersTable;
|
return collectionCiphersTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DataTable BuildCollectionUsersTable(SqlBulkCopy bulkCopy, IEnumerable<CollectionUser> collectionUsers)
|
||||||
|
{
|
||||||
|
var cu = collectionUsers.FirstOrDefault();
|
||||||
|
if (cu == null)
|
||||||
|
{
|
||||||
|
throw new ApplicationException("Must have some collectionUsers to bulk import.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var collectionUsersTable = new DataTable("CollectionUserDataTable");
|
||||||
|
|
||||||
|
var collectionIdColumn = new DataColumn(nameof(cu.CollectionId), cu.CollectionId.GetType());
|
||||||
|
collectionUsersTable.Columns.Add(collectionIdColumn);
|
||||||
|
var organizationUserIdColumn = new DataColumn(nameof(cu.OrganizationUserId), cu.OrganizationUserId.GetType());
|
||||||
|
collectionUsersTable.Columns.Add(organizationUserIdColumn);
|
||||||
|
var readOnlyColumn = new DataColumn(nameof(cu.ReadOnly), cu.ReadOnly.GetType());
|
||||||
|
collectionUsersTable.Columns.Add(readOnlyColumn);
|
||||||
|
var hidePasswordsColumn = new DataColumn(nameof(cu.HidePasswords), cu.HidePasswords.GetType());
|
||||||
|
collectionUsersTable.Columns.Add(hidePasswordsColumn);
|
||||||
|
var manageColumn = new DataColumn(nameof(cu.Manage), cu.Manage.GetType());
|
||||||
|
collectionUsersTable.Columns.Add(manageColumn);
|
||||||
|
|
||||||
|
foreach (DataColumn col in collectionUsersTable.Columns)
|
||||||
|
{
|
||||||
|
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = new DataColumn[2];
|
||||||
|
keys[0] = collectionIdColumn;
|
||||||
|
keys[1] = organizationUserIdColumn;
|
||||||
|
collectionUsersTable.PrimaryKey = keys;
|
||||||
|
|
||||||
|
foreach (var collectionUser in collectionUsers)
|
||||||
|
{
|
||||||
|
var row = collectionUsersTable.NewRow();
|
||||||
|
|
||||||
|
row[collectionIdColumn] = collectionUser.CollectionId;
|
||||||
|
row[organizationUserIdColumn] = collectionUser.OrganizationUserId;
|
||||||
|
row[readOnlyColumn] = collectionUser.ReadOnly;
|
||||||
|
row[hidePasswordsColumn] = collectionUser.HidePasswords;
|
||||||
|
row[manageColumn] = collectionUser.Manage;
|
||||||
|
|
||||||
|
collectionUsersTable.Rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectionUsersTable;
|
||||||
|
}
|
||||||
|
|
||||||
private DataTable BuildSendsTable(SqlBulkCopy bulkCopy, IEnumerable<Send> sends)
|
private DataTable BuildSendsTable(SqlBulkCopy bulkCopy, IEnumerable<Send> sends)
|
||||||
{
|
{
|
||||||
var s = sends.FirstOrDefault();
|
var s = sends.FirstOrDefault();
|
||||||
|
@ -161,7 +161,10 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers, IEnumerable<Core.Entities.Collection> collections, IEnumerable<Core.Entities.CollectionCipher> collectionCiphers)
|
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers,
|
||||||
|
IEnumerable<Core.Entities.Collection> collections,
|
||||||
|
IEnumerable<Core.Entities.CollectionCipher> collectionCiphers,
|
||||||
|
IEnumerable<Core.Entities.CollectionUser> collectionUsers)
|
||||||
{
|
{
|
||||||
if (!ciphers.Any())
|
if (!ciphers.Any())
|
||||||
{
|
{
|
||||||
@ -184,6 +187,13 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
|||||||
var collectionCipherEntities = Mapper.Map<List<CollectionCipher>>(collectionCiphers);
|
var collectionCipherEntities = Mapper.Map<List<CollectionCipher>>(collectionCiphers);
|
||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionCipherEntities);
|
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionCipherEntities);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collectionUsers.Any())
|
||||||
|
{
|
||||||
|
var collectionUserEntities = Mapper.Map<List<CollectionUser>>(collectionUsers);
|
||||||
|
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionUserEntities);
|
||||||
|
}
|
||||||
|
|
||||||
await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(ciphers.First().OrganizationId.Value);
|
await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(ciphers.First().OrganizationId.Value);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,9 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Models.Business;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
@ -21,6 +24,64 @@ namespace Bit.Core.Test.Services;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class CipherServiceTests
|
public class CipherServiceTests
|
||||||
{
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ImportCiphersAsync_IntoOrganization_Success(
|
||||||
|
Organization organization,
|
||||||
|
Guid importingUserId,
|
||||||
|
OrganizationUser importingOrganizationUser,
|
||||||
|
List<Collection> collections,
|
||||||
|
List<CipherDetails> ciphers,
|
||||||
|
SutProvider<CipherService> sutProvider)
|
||||||
|
{
|
||||||
|
organization.MaxCollections = null;
|
||||||
|
importingOrganizationUser.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
foreach (var collection in collections)
|
||||||
|
{
|
||||||
|
collection.OrganizationId = organization.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var cipher in ciphers)
|
||||||
|
{
|
||||||
|
cipher.OrganizationId = organization.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValuePair<int, int>[] collectionRelationships = {
|
||||||
|
new(0, 0),
|
||||||
|
new(1, 1),
|
||||||
|
new(2, 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetByOrganizationAsync(organization.Id, importingUserId)
|
||||||
|
.Returns(importingOrganizationUser);
|
||||||
|
|
||||||
|
// Set up a collection that already exists in the organization
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(new List<Collection> { collections[0] });
|
||||||
|
|
||||||
|
await sutProvider.Sut.ImportCiphersAsync(collections, ciphers, collectionRelationships, importingUserId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(
|
||||||
|
ciphers,
|
||||||
|
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count - 1 &&
|
||||||
|
!cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added
|
||||||
|
cols.All(c => collections.Any(x => c.Name == x.Name))),
|
||||||
|
Arg.Is<IEnumerable<CollectionCipher>>(c => c.Count() == ciphers.Count),
|
||||||
|
Arg.Is<IEnumerable<CollectionUser>>(cus =>
|
||||||
|
cus.Count() == collections.Count - 1 &&
|
||||||
|
!cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization
|
||||||
|
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
|
await sutProvider.GetDependency<IReferenceEventService>().Received(1).RaiseEventAsync(
|
||||||
|
Arg.Is<ReferenceEvent>(e => e.Type == ReferenceEventType.VaultImported));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task SaveAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider, Cipher cipher)
|
public async Task SaveAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider, Cipher cipher)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user