Skip to content

Commit 9a39d21

Browse files
committed
[PM-32743] Add ability to create folders during import to orgs
1 parent 28bd286 commit 9a39d21

10 files changed

Lines changed: 97 additions & 52 deletions

File tree

src/Api/Tools/Controllers/ImportCiphersController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public async Task PostImportOrganization([FromQuery] string organizationId,
7474
var orgId = new Guid(organizationId);
7575
var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();
7676

77-
//An User is allowed to import if CanCreate Collections or has AccessToImportExport
77+
// A User is allowed to import if CanCreate Collections or has AccessToImportExport
7878
var authorized = await CheckOrgImportPermissionAsync(collections, orgId);
7979
if (!authorized)
8080
{
@@ -83,7 +83,8 @@ public async Task PostImportOrganization([FromQuery] string organizationId,
8383

8484
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
8585
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
86-
await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);
86+
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
87+
await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId, folders, model.FolderRelationships);
8788
}
8889

8990
private async Task<bool> CheckOrgImportPermissionAsync(List<Collection> collections, Guid orgId)

src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ public class ImportOrganizationCiphersRequestModel
88
public CollectionWithIdRequestModel[] Collections { get; set; } = [];
99
public CipherRequestModel[] Ciphers { get; set; } = [];
1010
public KeyValuePair<int, int>[] CollectionRelationships { get; set; } = [];
11+
public FolderWithIdRequestModel[] Folders { get; set; } = [];
12+
public KeyValuePair<int, int>[] FolderRelationships { get; set; } = [];
1113
}

src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -75,34 +75,7 @@ public async Task ImportIntoIndividualVaultAsync(
7575
}
7676
}
7777

78-
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();
79-
80-
//Assign id to the ones that don't exist in DB
81-
//Need to keep the list order to create the relationships
82-
var newFolders = new List<Folder>();
83-
foreach (var folder in folders)
84-
{
85-
if (!userfoldersIds.Contains(folder.Id))
86-
{
87-
folder.SetNewId();
88-
newFolders.Add(folder);
89-
}
90-
}
91-
92-
// Create the folder associations based on the newly created folder ids
93-
foreach (var relationship in folderRelationships)
94-
{
95-
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
96-
var folder = folders.ElementAtOrDefault(relationship.Value);
97-
98-
if (cipher == null || folder == null)
99-
{
100-
continue;
101-
}
102-
103-
cipher.Folders = $"{{\"{cipher.UserId.ToString()!.ToUpperInvariant()}\":" +
104-
$"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
105-
}
78+
var newFolders = await processFolders(importingUserId, folders, folderRelationships, ciphers);
10679

10780
// Create it all
10881
await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
@@ -115,7 +88,9 @@ public async Task ImportIntoOrganizationalVaultAsync(
11588
List<Collection> collections,
11689
List<CipherDetails> ciphers,
11790
IEnumerable<KeyValuePair<int, int>> collectionRelationships,
118-
Guid importingUserId)
91+
Guid importingUserId,
92+
List<Folder> folders,
93+
IEnumerable<KeyValuePair<int, int>> folderRelationships)
11994
{
12095
var orgId = collections.Count > 0
12196
? collections[0].OrganizationId
@@ -163,6 +138,8 @@ public async Task ImportIntoOrganizationalVaultAsync(
163138
}
164139
}
165140

141+
var newFolders = await processFolders(importingUserId, folders, folderRelationships, ciphers);
142+
166143
var organizationCollectionsIds = (await _collectionRepository.GetManyByOrganizationIdAsync(org.Id)).Select(c => c.Id).ToList();
167144

168145
//Assign id to the ones that don't exist in DB
@@ -222,9 +199,43 @@ public async Task ImportIntoOrganizationalVaultAsync(
222199
}
223200

224201
// Create it all
225-
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers);
202+
await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers, newFolders);
226203

227204
// push
228205
await _pushService.PushSyncVaultAsync(importingUserId);
229206
}
207+
208+
private async Task<List<Folder>> processFolders(Guid importingUserId, List<Folder> folders, IEnumerable<KeyValuePair<int, int>> folderRelationships, List<CipherDetails> ciphers)
209+
{
210+
var userfoldersIds = (await _folderRepository.GetManyByUserIdAsync(importingUserId)).Select(f => f.Id).ToList();
211+
212+
//Assign id to the ones that don't exist in DB
213+
//Need to keep the list order to create the relationships
214+
var newFolders = new List<Folder>();
215+
foreach (var folder in folders)
216+
{
217+
if (!userfoldersIds.Contains(folder.Id))
218+
{
219+
folder.SetNewId();
220+
newFolders.Add(folder);
221+
}
222+
}
223+
224+
// Create the folder associations based on the newly created folder ids
225+
foreach (var relationship in folderRelationships)
226+
{
227+
var cipher = ciphers.ElementAtOrDefault(relationship.Key);
228+
var folder = folders.ElementAtOrDefault(relationship.Value);
229+
230+
if (cipher == null || folder == null)
231+
{
232+
continue;
233+
}
234+
235+
cipher.Folders = $"{{\"{importingUserId.ToString().ToUpperInvariant()}\":" +
236+
$"\"{folder.Id.ToString().ToUpperInvariant()}\"}}";
237+
}
238+
239+
return newFolders;
240+
}
230241
}

src/Core/Tools/ImportFeatures/Interfaces/IImportCiphersCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ Task ImportIntoIndividualVaultAsync(List<Folder> folders, List<CipherDetails> ci
1010
IEnumerable<KeyValuePair<int, int>> folderRelationships, Guid importingUserId);
1111

1212
Task ImportIntoOrganizationalVaultAsync(List<Collection> collections, List<CipherDetails> ciphers,
13-
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId);
13+
IEnumerable<KeyValuePair<int, int>> collectionRelationships, Guid importingUserId,
14+
List<Folder> folders, IEnumerable<KeyValuePair<int, int>> folderRelationships);
1415
}

src/Core/Vault/Repositories/ICipherRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
3838
/// </summary>
3939
Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
4040
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
41-
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
41+
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers, IEnumerable<Folder> folders);
4242
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
4343
Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable<Guid> ids, Guid organizationId);
4444
Task<DateTime> UnarchiveAsync(IEnumerable<Guid> ids, Guid userId);

src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ await connection.ExecuteAsync(
527527
}
528528

529529
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
530-
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers)
530+
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers, IEnumerable<Folder> folders)
531531
{
532532
if (!ciphers.Any())
533533
{
@@ -559,6 +559,11 @@ public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collectio
559559
await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);
560560
}
561561

562+
if (folders.Any())
563+
{
564+
await BulkResourceCreationService.CreateFoldersAsync(connection, transaction, folders);
565+
}
566+
562567
await connection.ExecuteAsync(
563568
$"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]",
564569
new { OrganizationId = ciphers.First().OrganizationId },

src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ public async Task CreateAsync(Guid userId, IEnumerable<Core.Vault.Entities.Ciphe
173173
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers,
174174
IEnumerable<Core.Entities.Collection> collections,
175175
IEnumerable<Core.Entities.CollectionCipher> collectionCiphers,
176-
IEnumerable<Core.Entities.CollectionUser> collectionUsers)
176+
IEnumerable<Core.Entities.CollectionUser> collectionUsers,
177+
IEnumerable<Core.Vault.Entities.Folder> folders)
177178
{
178179
if (!ciphers.Any())
179180
{
@@ -203,6 +204,12 @@ public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers,
203204
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, collectionUserEntities);
204205
}
205206

207+
if(folders.Any())
208+
{
209+
var folderEntities = Mapper.Map<List<Folder>>(folders);
210+
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
211+
}
212+
206213
await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(ciphers.First().OrganizationId.Value);
207214
await dbContext.SaveChangesAsync();
208215
}

test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
228228
Arg.Any<List<Collection>>(),
229229
Arg.Any<List<CipherDetails>>(),
230230
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
231-
Arg.Any<Guid>());
231+
Arg.Any<Guid>(),
232+
Arg.Any<List<Folder>>(),
233+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
232234
}
233235

234236
[Theory, BitAutoData]
@@ -300,7 +302,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
300302
Arg.Any<List<Collection>>(),
301303
Arg.Any<List<CipherDetails>>(),
302304
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
303-
Arg.Any<Guid>());
305+
Arg.Any<Guid>(),
306+
Arg.Any<List<Folder>>(),
307+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
304308
}
305309

306310
[Theory, BitAutoData]
@@ -503,7 +507,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
503507
Arg.Any<List<Collection>>(),
504508
Arg.Any<List<CipherDetails>>(),
505509
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
506-
Arg.Any<Guid>());
510+
Arg.Any<Guid>(),
511+
Arg.Any<List<Folder>>(),
512+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
507513
}
508514

509515
[Theory, BitAutoData]
@@ -584,7 +590,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
584590
Arg.Any<List<Collection>>(),
585591
Arg.Any<List<CipherDetails>>(),
586592
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
587-
Arg.Any<Guid>());
593+
Arg.Any<Guid>(),
594+
Arg.Any<List<Folder>>(),
595+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
588596
}
589597

590598
[Theory, BitAutoData]
@@ -658,7 +666,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
658666
Arg.Any<List<Collection>>(),
659667
Arg.Any<List<CipherDetails>>(),
660668
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
661-
Arg.Any<Guid>());
669+
Arg.Any<Guid>(),
670+
Arg.Any<List<Folder>>(),
671+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
662672
}
663673

664674
[Theory, BitAutoData]
@@ -734,7 +744,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
734744
Arg.Any<List<Collection>>(),
735745
Arg.Any<List<CipherDetails>>(),
736746
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
737-
Arg.Any<Guid>());
747+
Arg.Any<Guid>(),
748+
Arg.Any<List<Folder>>(),
749+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
738750
}
739751

740752
[Theory, BitAutoData]
@@ -804,7 +816,9 @@ await sutProvider.GetDependency<IImportCiphersCommand>()
804816
Arg.Any<List<Collection>>(),
805817
Arg.Any<List<CipherDetails>>(),
806818
Arg.Any<IEnumerable<KeyValuePair<int, int>>>(),
807-
Arg.Any<Guid>());
819+
Arg.Any<Guid>(),
820+
Arg.Any<List<Folder>>(),
821+
Arg.Any<IEnumerable<KeyValuePair<int, int>>>());
808822
}
809823

810824
private static void SetupUserService(SutProvider<ImportCiphersController> sutProvider, User user)

test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public async Task ImportIntoOrganizationalVaultAsync_Success(
149149
.GetManyByOrganizationIdAsync(organization.Id)
150150
.Returns(new List<Collection> { collections[0] });
151151

152-
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
152+
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId, [], []);
153153

154154
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(
155155
ciphers,
@@ -160,7 +160,8 @@ await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(
160160
Arg.Is<IEnumerable<CollectionUser>>(cus =>
161161
cus.Count() == collections.Count - 1 &&
162162
!cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization
163-
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)));
163+
cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true)),
164+
Arg.Is<IEnumerable<Folder>>(f => f.Count() == 0));
164165
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
165166
}
166167

@@ -206,7 +207,7 @@ public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException(
206207
.Returns(new List<Collection> { collections[0] });
207208

208209
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
209-
sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId));
210+
sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId, [], []));
210211

211212
Assert.Equal("This organization can only have a maximum of " +
212213
$"{organization.MaxCollections} collections.", exception.Message);
@@ -256,14 +257,15 @@ public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_Sk
256257
.GetManyByOrganizationIdAsync(organization.Id)
257258
.Returns(new List<Collection>());
258259

259-
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
260+
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId, [], []);
260261

261262
// Verify ciphers were created but no CollectionUser entries were created (because the organization user (importingUserId) is null)
262263
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(
263264
ciphers,
264265
Arg.Is<IEnumerable<Collection>>(cols => cols.Count() == collections.Count),
265266
Arg.Is<IEnumerable<CollectionCipher>>(cc => cc.Count() == ciphers.Count),
266-
Arg.Is<IEnumerable<CollectionUser>>(cus => !cus.Any()));
267+
Arg.Is<IEnumerable<CollectionUser>>(cus => !cus.Any()),
268+
Arg.Is<IEnumerable<Folder>>(f => f.Count() == 0));
267269

268270
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
269271
}
@@ -309,7 +311,7 @@ public async Task ImportIntoOrganizationalVaultAsync_WithNullImportingOrgUser_An
309311
.Returns(false);
310312

311313
var exception = await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
312-
sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId));
314+
sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId, [], []));
313315

314316
Assert.Contains("organization members or authorized providers", exception.Message);
315317
}
@@ -398,7 +400,7 @@ public async Task ImportIntoOrganizationalVaultAsync_WithArchivedCiphers_SetsArc
398400
.GetManyByOrganizationIdAsync(organization.Id)
399401
.Returns(new List<Collection>());
400402

401-
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId);
403+
await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId, [], []);
402404

403405
await sutProvider.GetDependency<ICipherRepository>()
404406
.Received(1)
@@ -410,6 +412,7 @@ await sutProvider.GetDependency<ICipherRepository>()
410412
c[0].Archives.Contains(archivedDate.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"))),
411413
Arg.Any<IEnumerable<Collection>>(),
412414
Arg.Any<IEnumerable<CollectionCipher>>(),
413-
Arg.Any<IEnumerable<CollectionUser>>());
415+
Arg.Any<IEnumerable<CollectionUser>>(),
416+
Arg.Is<IEnumerable<Folder>>(f => f.Count() == 0));
414417
}
415418
}

test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,8 @@ await cipherRepository.CreateAsync(
10281028
ciphers: [cipher],
10291029
collections: [collection],
10301030
collectionCiphers: [collectionCipher],
1031-
collectionUsers: [collectionUser]);
1031+
collectionUsers: [collectionUser],
1032+
[]);
10321033

10331034
// Assert
10341035
var orgCiphers = await cipherRepository.GetManyByOrganizationIdAsync(org.Id);

0 commit comments

Comments
 (0)