mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-27083] Prevent collection nesting on import into a MyItems-collection (#16937)
* Prevent collection nesting on import into a my items collection My Items collections do not support nested collections. The import source hierarchy needs to be flattened into the My Items collection * Introduce new types for folder and collection relationship Makes it easier to identify which position is for the cipherIndex and which is for the folder-/collection-index * Fix assignment of ciphers to My items collection * Remove unneeded type cast or assertions * Add clarifying comment --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
326cd40628
commit
2dd314e992
@@ -6,12 +6,15 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
|
export type FolderRelationship = [cipherIndex: number, folderIndex: number];
|
||||||
|
export type CollectionRelationship = [cipherIndex: number, collectionIndex: number];
|
||||||
|
|
||||||
export class ImportResult {
|
export class ImportResult {
|
||||||
success = false;
|
success = false;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
ciphers: CipherView[] = [];
|
ciphers: CipherView[] = [];
|
||||||
folders: FolderView[] = [];
|
folders: FolderView[] = [];
|
||||||
folderRelationships: [number, number][] = [];
|
folderRelationships: FolderRelationship[] = [];
|
||||||
collections: CollectionView[] = [];
|
collections: CollectionView[] = [];
|
||||||
collectionRelationships: [number, number][] = [];
|
collectionRelationships: CollectionRelationship[] = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { mock, MockProxy } from "jest-mock-extended";
|
|||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
import {
|
||||||
|
CollectionService,
|
||||||
|
CollectionTypes,
|
||||||
|
CollectionView,
|
||||||
|
} from "@bitwarden/admin-console/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||||
@@ -194,7 +198,7 @@ describe("ImportService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passing importTarget as null on setImportTarget with organizationId throws error", async () => {
|
it("passing importTarget as undefined on setImportTarget with organizationId throws error", async () => {
|
||||||
const setImportTargetMethod = importService["setImportTarget"](
|
const setImportTargetMethod = importService["setImportTarget"](
|
||||||
null,
|
null,
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -204,10 +208,10 @@ describe("ImportService", () => {
|
|||||||
await expect(setImportTargetMethod).rejects.toThrow();
|
await expect(setImportTargetMethod).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passing importTarget as null on setImportTarget throws error", async () => {
|
it("passing importTarget as undefined on setImportTarget throws error", async () => {
|
||||||
const setImportTargetMethod = importService["setImportTarget"](
|
const setImportTargetMethod = importService["setImportTarget"](
|
||||||
null,
|
null,
|
||||||
"",
|
undefined,
|
||||||
new Object() as CollectionView,
|
new Object() as CollectionView,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,11 +243,40 @@ describe("ImportService", () => {
|
|||||||
importResult.ciphers.push(createCipher({ name: "cipher2" }));
|
importResult.ciphers.push(createCipher({ name: "cipher2" }));
|
||||||
importResult.folderRelationships.push([0, 0]);
|
importResult.folderRelationships.push([0, 0]);
|
||||||
|
|
||||||
await importService["setImportTarget"](importResult, "", mockImportTargetFolder);
|
await importService["setImportTarget"](importResult, undefined, mockImportTargetFolder);
|
||||||
expect(importResult.folderRelationships.length).toEqual(2);
|
expect(importResult.folderRelationships.length).toEqual(2);
|
||||||
expect(importResult.folderRelationships[0]).toEqual([1, 0]);
|
expect(importResult.folderRelationships[0]).toEqual([1, 0]);
|
||||||
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("If importTarget is of type DefaultUserCollection sets it as new root for all ciphers as nesting is not supported", async () => {
|
||||||
|
importResult.collections.push(mockCollection1);
|
||||||
|
importResult.collections.push(mockCollection2);
|
||||||
|
importResult.ciphers.push(createCipher({ name: "cipher1" }));
|
||||||
|
importResult.ciphers.push(createCipher({ name: "cipher2" }));
|
||||||
|
importResult.ciphers.push(createCipher({ name: "cipher3" }));
|
||||||
|
|
||||||
|
importResult.collectionRelationships.push([0, 0]);
|
||||||
|
importResult.collectionRelationships.push([1, 1]);
|
||||||
|
importResult.collectionRelationships.push([2, 0]);
|
||||||
|
|
||||||
|
mockImportTargetCollection.type = CollectionTypes.DefaultUserCollection;
|
||||||
|
await importService["setImportTarget"](
|
||||||
|
importResult,
|
||||||
|
organizationId,
|
||||||
|
mockImportTargetCollection,
|
||||||
|
);
|
||||||
|
expect(importResult.collections.length).toBe(1);
|
||||||
|
expect(importResult.collections[0]).toBe(mockImportTargetCollection);
|
||||||
|
|
||||||
|
expect(importResult.collectionRelationships.length).toEqual(3);
|
||||||
|
expect(importResult.collectionRelationships[0]).toEqual([0, 0]);
|
||||||
|
expect(importResult.collectionRelationships[1]).toEqual([1, 0]);
|
||||||
|
expect(importResult.collectionRelationships[2]).toEqual([2, 0]);
|
||||||
|
|
||||||
|
expect(importResult.collectionRelationships.map((r) => r[0])).toEqual([0, 1, 2]);
|
||||||
|
expect(importResult.collectionRelationships.every((r) => r[1] === 0)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CollectionService,
|
CollectionService,
|
||||||
CollectionWithIdRequest,
|
CollectionWithIdRequest,
|
||||||
CollectionView,
|
CollectionView,
|
||||||
|
CollectionTypes,
|
||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
@@ -101,7 +102,7 @@ import {
|
|||||||
ImportType,
|
ImportType,
|
||||||
regularImportOptions,
|
regularImportOptions,
|
||||||
} from "../models/import-options";
|
} from "../models/import-options";
|
||||||
import { ImportResult } from "../models/import-result";
|
import { CollectionRelationship, FolderRelationship, ImportResult } from "../models/import-result";
|
||||||
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
||||||
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
||||||
|
|
||||||
@@ -473,19 +474,20 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
|
|
||||||
private async setImportTarget(
|
private async setImportTarget(
|
||||||
importResult: ImportResult,
|
importResult: ImportResult,
|
||||||
organizationId: string,
|
organizationId: OrganizationId | undefined,
|
||||||
importTarget: FolderView | CollectionView,
|
importTarget: FolderView | CollectionView,
|
||||||
) {
|
) {
|
||||||
if (!importTarget) {
|
if (!importTarget) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Importing into an organization
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
if (!(importTarget instanceof CollectionView)) {
|
if (!(importTarget instanceof CollectionView)) {
|
||||||
throw new Error(this.i18nService.t("errorAssigningTargetCollection"));
|
throw new Error(this.i18nService.t("errorAssigningTargetCollection"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const noCollectionRelationShips: [number, number][] = [];
|
const noCollectionRelationShips: CollectionRelationship[] = [];
|
||||||
importResult.ciphers.forEach((c, index) => {
|
importResult.ciphers.forEach((c, index) => {
|
||||||
if (
|
if (
|
||||||
!Array.isArray(importResult.collectionRelationships) ||
|
!Array.isArray(importResult.collectionRelationships) ||
|
||||||
@@ -495,15 +497,28 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const collections: CollectionView[] = [...importResult.collections];
|
// My Items collections do not support collection nesting.
|
||||||
importResult.collections = [importTarget as CollectionView];
|
// Flatten all ciphers from nested collections into the import target.
|
||||||
|
if (importTarget.type === CollectionTypes.DefaultUserCollection) {
|
||||||
|
importResult.collections = [importTarget];
|
||||||
|
|
||||||
|
const flattenRelationships: CollectionRelationship[] = [];
|
||||||
|
importResult.ciphers.forEach((c, index) => {
|
||||||
|
flattenRelationships.push([index, 0]);
|
||||||
|
});
|
||||||
|
importResult.collectionRelationships = flattenRelationships;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collections = [...importResult.collections];
|
||||||
|
importResult.collections = [importTarget];
|
||||||
collections.map((x) => {
|
collections.map((x) => {
|
||||||
const f = new CollectionView(x);
|
const f = new CollectionView(x);
|
||||||
f.name = `${importTarget.name}/${x.name}`;
|
f.name = `${importTarget.name}/${x.name}`;
|
||||||
importResult.collections.push(f);
|
importResult.collections.push(f);
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationships: [number, number][] = [...importResult.collectionRelationships];
|
const relationships = [...importResult.collectionRelationships];
|
||||||
importResult.collectionRelationships = [...noCollectionRelationShips];
|
importResult.collectionRelationships = [...noCollectionRelationShips];
|
||||||
relationships.map((x) => {
|
relationships.map((x) => {
|
||||||
importResult.collectionRelationships.push([x[0], x[1] + 1]);
|
importResult.collectionRelationships.push([x[0], x[1] + 1]);
|
||||||
@@ -512,11 +527,12 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Importing into personal vault
|
||||||
if (!(importTarget instanceof FolderView)) {
|
if (!(importTarget instanceof FolderView)) {
|
||||||
throw new Error(this.i18nService.t("errorAssigningTargetFolder"));
|
throw new Error(this.i18nService.t("errorAssigningTargetFolder"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const noFolderRelationShips: [number, number][] = [];
|
const noFolderRelationShips: FolderRelationship[] = [];
|
||||||
importResult.ciphers.forEach((c, index) => {
|
importResult.ciphers.forEach((c, index) => {
|
||||||
if (Utils.isNullOrEmpty(c.folderId)) {
|
if (Utils.isNullOrEmpty(c.folderId)) {
|
||||||
c.folderId = importTarget.id;
|
c.folderId = importTarget.id;
|
||||||
@@ -524,8 +540,8 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const folders: FolderView[] = [...importResult.folders];
|
const folders = [...importResult.folders];
|
||||||
importResult.folders = [importTarget as FolderView];
|
importResult.folders = [importTarget];
|
||||||
folders.map((x) => {
|
folders.map((x) => {
|
||||||
const newFolderName = `${importTarget.name}/${x.name}`;
|
const newFolderName = `${importTarget.name}/${x.name}`;
|
||||||
const f = new FolderView();
|
const f = new FolderView();
|
||||||
@@ -533,7 +549,7 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
importResult.folders.push(f);
|
importResult.folders.push(f);
|
||||||
});
|
});
|
||||||
|
|
||||||
const relationships: [number, number][] = [...importResult.folderRelationships];
|
const relationships = [...importResult.folderRelationships];
|
||||||
importResult.folderRelationships = [...noFolderRelationShips];
|
importResult.folderRelationships = [...noFolderRelationShips];
|
||||||
relationships.map((x) => {
|
relationships.map((x) => {
|
||||||
importResult.folderRelationships.push([x[0], x[1] + 1]);
|
importResult.folderRelationships.push([x[0], x[1] + 1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user