diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 4a766fb80a..4dc4dfa749 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -112,7 +112,7 @@ namespace Bit.Api.Controllers } [HttpPost("import")] - public async Task PostImport([FromBody]ImportPasswordsRequestModel model) + public async Task PostImport([FromBody]ImportCiphersRequestModel model) { var userId = _userService.GetProperUserId(User).Value; var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList(); @@ -120,6 +120,21 @@ namespace Bit.Api.Controllers await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships); } + [HttpPost("{orgId}/import")] + public async Task PostImport(string orgId, [FromBody]ImportOrganizationCiphersRequestModel model) + { + var organizationId = new Guid(orgId); + if(!_currentContext.OrganizationAdmin(organizationId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User).Value; + var collections = model.Collections.Select(c => c.ToCollection(organizationId)).ToList(); + var ciphers = model.Logins.Select(l => l.ToOrganizationCipherDetails(organizationId)).ToList(); + await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId); + } + [HttpPut("{id}/partial")] [HttpPost("{id}/partial")] public async Task PutPartial(string id, [FromBody]CipherPartialRequestModel model) diff --git a/src/Core/Models/Api/Request/Accounts/ImportCiphersRequestModel.cs b/src/Core/Models/Api/Request/Accounts/ImportCiphersRequestModel.cs new file mode 100644 index 0000000000..079188ec26 --- /dev/null +++ b/src/Core/Models/Api/Request/Accounts/ImportCiphersRequestModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class ImportCiphersRequestModel + { + public FolderRequestModel[] Folders { get; set; } + public LoginRequestModel[] Logins { get; set; } + public KeyValuePair[] FolderRelationships { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs b/src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs deleted file mode 100644 index 8ab229c8ae..0000000000 --- a/src/Core/Models/Api/Request/Accounts/ImportPasswordsRequestModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Bit.Core.Models.Api -{ - public class ImportPasswordsRequestModel - { - private LoginRequestModel[] _logins; - - public FolderRequestModel[] Folders { get; set; } - [Obsolete] - public LoginRequestModel[] Sites - { - get { return _logins; } - set { _logins = value; } - } - public LoginRequestModel[] Logins - { - get { return _logins; } - set { _logins = value; } - } - public KeyValuePair[] FolderRelationships { get; set; } - } -} diff --git a/src/Core/Models/Api/Request/LoginRequestModel.cs b/src/Core/Models/Api/Request/LoginRequestModel.cs index 2d06638e42..2e6ee69fe2 100644 --- a/src/Core/Models/Api/Request/LoginRequestModel.cs +++ b/src/Core/Models/Api/Request/LoginRequestModel.cs @@ -44,6 +44,15 @@ namespace Bit.Core.Models.Api }); } + public CipherDetails ToOrganizationCipherDetails(Guid orgId) + { + return ToCipherDetails(new CipherDetails + { + OrganizationId = orgId, + Edit = true + }); + } + public Cipher ToOrganizationCipher() { if(string.IsNullOrWhiteSpace(OrganizationId)) diff --git a/src/Core/Models/Api/Request/Organizations/ImportOrganizationCiphersRequestModel.cs b/src/Core/Models/Api/Request/Organizations/ImportOrganizationCiphersRequestModel.cs new file mode 100644 index 0000000000..b2decbb1cb --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/ImportOrganizationCiphersRequestModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Api +{ + public class ImportOrganizationCiphersRequestModel + { + public CollectionRequestModel[] Collections { get; set; } + public LoginRequestModel[] Logins { get; set; } + public KeyValuePair[] CollectionRelationships { get; set; } + } +} diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 4e6f96251b..df174d9567 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -26,5 +26,7 @@ namespace Bit.Core.Repositories Task MoveAsync(IEnumerable ids, Guid? folderId, Guid userId); Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable folders); + Task CreateAsync(IEnumerable ciphers, IEnumerable collections, + IEnumerable collectionCiphers); } } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index e4e7e8d573..305c93fba7 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -401,6 +401,65 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task CreateAsync(IEnumerable ciphers, IEnumerable collections, + IEnumerable collectionCiphers) + { + if(!ciphers.Any()) + { + return; + } + + using(var connection = new SqlConnection(ConnectionString)) + { + connection.Open(); + + using(var transaction = connection.BeginTransaction()) + { + try + { + using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "[dbo].[Cipher]"; + var dataTable = BuildCiphersTable(ciphers); + bulkCopy.WriteToServer(dataTable); + } + + if(collections.Any()) + { + using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "[dbo].[Collection]"; + var dataTable = BuildCollectionsTable(collections); + bulkCopy.WriteToServer(dataTable); + } + + if(collectionCiphers.Any()) + { + using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "[dbo].[CollectionCipher]"; + var dataTable = BuildCollectionCiphersTable(collectionCiphers); + bulkCopy.WriteToServer(dataTable); + } + } + } + + await connection.ExecuteAsync( + $"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]", + new { OrganizationId = ciphers.First().OrganizationId }, + commandType: CommandType.StoredProcedure, transaction: transaction); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + } + } + private DataTable BuildCiphersTable(IEnumerable ciphers) { var c = ciphers.FirstOrDefault(); @@ -498,6 +557,80 @@ namespace Bit.Core.Repositories.SqlServer return foldersTable; } + private DataTable BuildCollectionsTable(IEnumerable collections) + { + var c = collections.FirstOrDefault(); + if(c == null) + { + throw new ApplicationException("Must have some collections to bulk import."); + } + + var collectionsTable = new DataTable("CollectionDataTable"); + + var idColumn = new DataColumn(nameof(c.Id), c.Id.GetType()); + collectionsTable.Columns.Add(idColumn); + var organizationIdColumn = new DataColumn(nameof(c.OrganizationId), c.OrganizationId.GetType()); + collectionsTable.Columns.Add(organizationIdColumn); + var nameColumn = new DataColumn(nameof(c.Name), typeof(string)); + collectionsTable.Columns.Add(nameColumn); + var creationDateColumn = new DataColumn(nameof(c.CreationDate), c.CreationDate.GetType()); + collectionsTable.Columns.Add(creationDateColumn); + var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType()); + collectionsTable.Columns.Add(revisionDateColumn); + + var keys = new DataColumn[1]; + keys[0] = idColumn; + collectionsTable.PrimaryKey = keys; + + foreach(var collection in collections) + { + var row = collectionsTable.NewRow(); + + row[idColumn] = collection.Id; + row[organizationIdColumn] = collection.OrganizationId; + row[nameColumn] = collection.Name; + row[creationDateColumn] = collection.CreationDate; + row[revisionDateColumn] = collection.RevisionDate; + + collectionsTable.Rows.Add(row); + } + + return collectionsTable; + } + + private DataTable BuildCollectionCiphersTable(IEnumerable collectionCiphers) + { + var cc = collectionCiphers.FirstOrDefault(); + if(cc == null) + { + throw new ApplicationException("Must have some collectionCiphers to bulk import."); + } + + var collectionCiphersTable = new DataTable("CollectionCipherDataTable"); + + var collectionIdColumn = new DataColumn(nameof(cc.CollectionId), cc.CollectionId.GetType()); + collectionCiphersTable.Columns.Add(collectionIdColumn); + var cipherIdColumn = new DataColumn(nameof(cc.CipherId), cc.CipherId.GetType()); + collectionCiphersTable.Columns.Add(cipherIdColumn); + + var keys = new DataColumn[2]; + keys[0] = collectionIdColumn; + keys[1] = cipherIdColumn; + collectionCiphersTable.PrimaryKey = keys; + + foreach(var collectionCipher in collectionCiphers) + { + var row = collectionCiphersTable.NewRow(); + + row[collectionIdColumn] = collectionCipher.CollectionId; + row[cipherIdColumn] = collectionCipher.CipherId; + + collectionCiphersTable.Rows.Add(row); + } + + return collectionCiphersTable; + } + public class CipherWithCollections : Cipher { public DataTable CollectionIds { get; set; } diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index e9d08c6d7f..b81c76e139 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -25,5 +25,7 @@ namespace Bit.Core.Services Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin); Task ImportCiphersAsync(List folders, List ciphers, IEnumerable> folderRelationships); + Task ImportCiphersAsync(List collections, List ciphers, + IEnumerable> collectionRelationships, Guid importingUserId); } } diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index c987e6d86b..4db7b46d65 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -464,6 +464,50 @@ namespace Bit.Core.Services } } + public async Task ImportCiphersAsync( + List collections, + List ciphers, + IEnumerable> collectionRelationships, + Guid importingUserId) + { + // Init. ids for ciphers + foreach(var cipher in ciphers) + { + cipher.SetNewId(); + } + + // Init. ids for collections + foreach(var collection in collections) + { + collection.SetNewId(); + } + + // Create associations based on the newly assigned ids + var collectionCiphers = new List(); + foreach(var relationship in collectionRelationships) + { + var cipher = ciphers.ElementAtOrDefault(relationship.Key); + var collection = collections.ElementAtOrDefault(relationship.Value); + + if(cipher == null || collection == null) + { + continue; + } + + collectionCiphers.Add(new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = collection.Id + }); + } + + // Create it all + await _cipherRepository.CreateAsync(ciphers, collections, collectionCiphers); + + // push + await _pushService.PushSyncVaultAsync(importingUserId); + } + private async Task UserCanEditAsync(Cipher cipher, Guid userId) { if(!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId)