1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01: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:
Rui Tomé 2023-11-23 12:21:20 +00:00 committed by GitHub
parent 4e8284cf81
commit e2d644f136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 9 deletions

View File

@ -32,7 +32,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
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 SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
Task<DateTime> RestoreAsync(IEnumerable<Guid> ids, Guid userId);

View File

@ -27,6 +27,7 @@ public class CipherService : ICipherService
private readonly ICollectionRepository _collectionRepository;
private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionCipherRepository _collectionCipherRepository;
private readonly IPushNotificationService _pushService;
private readonly IAttachmentStorageService _attachmentStorageService;
@ -34,7 +35,7 @@ public class CipherService : ICipherService
private readonly IUserService _userService;
private readonly IPolicyService _policyService;
private readonly GlobalSettings _globalSettings;
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private const long _fileSizeLeeway = 1024L * 1024L; // 1MB
private readonly IReferenceEventService _referenceEventService;
private readonly ICurrentContext _currentContext;
@ -44,6 +45,7 @@ public class CipherService : ICipherService
ICollectionRepository collectionRepository,
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionCipherRepository collectionCipherRepository,
IPushNotificationService pushService,
IAttachmentStorageService attachmentStorageService,
@ -59,6 +61,7 @@ public class CipherService : ICipherService
_collectionRepository = collectionRepository;
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_collectionCipherRepository = collectionCipherRepository;
_pushService = pushService;
_attachmentStorageService = attachmentStorageService;
@ -652,7 +655,7 @@ public class CipherService : ICipherService
cipher.RevisionDate = DateTime.UtcNow;
// The sprocs will validate that all collections belong to this org/user and that they have
// The sprocs will validate that all collections belong to this org/user and that they have
// proper write permissions.
if (orgAdmin)
{
@ -747,6 +750,7 @@ public class CipherService : ICipherService
var org = collections.Count > 0 ?
await _organizationRepository.GetByIdAsync(collections[0].OrganizationId) :
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)
{
@ -764,18 +768,25 @@ public class CipherService : ICipherService
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
//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)
{
if (!userCollectionsIds.Contains(collection.Id))
if (!organizationCollectionsIds.Contains(collection.Id))
{
collection.SetNewId();
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
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers);
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
// push
await _pushService.PushSyncVaultAsync(importingUserId);

View File

@ -589,7 +589,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
}
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers)
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers)
{
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(
$"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]",
new { OrganizationId = ciphers.First().OrganizationId },
@ -896,6 +906,53 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
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)
{
var s = sends.FirstOrDefault();

View File

@ -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())
{
@ -184,6 +187,13 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
var collectionCipherEntities = Mapper.Map<List<CollectionCipher>>(collectionCiphers);
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.SaveChangesAsync();
}

View File

@ -4,6 +4,9 @@ using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
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.Vault.Entities;
using Bit.Core.Vault.Models.Data;
@ -21,6 +24,64 @@ namespace Bit.Core.Test.Services;
[SutProviderCustomize]
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]
public async Task SaveAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider, Cipher cipher)
{