1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

[PM-24227] Enable TS-strict for Collection Domain models (#15765)

* wip ts-strict

* wip ts-strict

* wip

* cleanup

* cleanup

* fix story

* fix story

* fix story

* wip

* clean up CollectionAdminView construction

* fix deprecated function call

* fix cli

* clean up

* fix story

* wip

* fix cli

* requested changes

* clean up, fixing minor bugs, more type saftey

* assign props in static ctor, clean up
This commit is contained in:
Brandon Treston
2025-08-14 13:08:24 -04:00
committed by GitHub
parent ac7e873813
commit 27089fbb57
41 changed files with 537 additions and 379 deletions

View File

@@ -1,7 +1,10 @@
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { OrgKey } from "@bitwarden/common/types/key";
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
import { CollectionAccessDetailsResponse } from "./collection.response";
import { CollectionAccessDetailsResponse, CollectionResponse } from "./collection.response";
import { CollectionView } from "./collection.view";
// TODO: this is used to represent the pseudo "Unassigned" collection as well as
@@ -24,24 +27,6 @@ export class CollectionAdminView extends CollectionView {
*/
assigned: boolean = false;
constructor(response?: CollectionAccessDetailsResponse) {
super(response);
if (!response) {
return;
}
this.groups = response.groups
? response.groups.map((g) => new CollectionAccessSelectionView(g))
: [];
this.users = response.users
? response.users.map((g) => new CollectionAccessSelectionView(g))
: [];
this.assigned = response.assigned;
}
/**
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
*/
@@ -115,4 +100,46 @@ export class CollectionAdminView extends CollectionView {
get isUnassignedCollection() {
return this.id === Unassigned;
}
static async fromCollectionAccessDetails(
collection: CollectionAccessDetailsResponse,
encryptService: EncryptService,
orgKey: OrgKey,
): Promise<CollectionAdminView> {
const view = new CollectionAdminView({ ...collection });
view.name = await encryptService.decryptString(new EncString(view.name), orgKey);
view.assigned = collection.assigned;
view.readOnly = collection.readOnly;
view.hidePasswords = collection.hidePasswords;
view.manage = collection.manage;
view.unmanaged = collection.unmanaged;
view.type = collection.type;
view.externalId = collection.externalId;
view.groups = collection.groups
? collection.groups.map((g) => new CollectionAccessSelectionView(g))
: [];
view.users = collection.users
? collection.users.map((g) => new CollectionAccessSelectionView(g))
: [];
return view;
}
static async fromCollectionResponse(
collection: CollectionResponse,
encryptService: EncryptService,
orgKey: OrgKey,
): Promise<CollectionAdminView> {
const collectionAdminView = new CollectionAdminView({
id: collection.id,
name: await encryptService.decryptString(new EncString(collection.name), orgKey),
organizationId: collection.organizationId,
});
collectionAdminView.externalId = collection.externalId;
return collectionAdminView;
}
}

View File

@@ -10,7 +10,10 @@ export class CollectionWithIdRequest extends CollectionRequest {
if (collection == null) {
return;
}
super(collection);
super({
name: collection.name,
externalId: collection.externalId,
});
this.id = collection.id;
}
}

View File

@@ -2,18 +2,18 @@ import { Jsonify } from "type-fest";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CollectionType } from "./collection";
import { CollectionType, CollectionTypes } from "./collection";
import { CollectionDetailsResponse } from "./collection.response";
export class CollectionData {
id: CollectionId;
organizationId: OrganizationId;
name: string;
externalId: string;
readOnly: boolean;
manage: boolean;
hidePasswords: boolean;
type: CollectionType;
externalId: string | undefined;
readOnly: boolean = false;
manage: boolean = false;
hidePasswords: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
constructor(response: CollectionDetailsResponse) {
this.id = response.id;

View File

@@ -1,20 +1,30 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { Collection } from "./collection";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
export class CollectionRequest {
name: string;
externalId: string;
externalId: string | undefined;
groups: SelectionReadOnlyRequest[] = [];
users: SelectionReadOnlyRequest[] = [];
constructor(collection?: Collection) {
if (collection == null) {
return;
constructor(c: {
name: EncString;
users?: SelectionReadOnlyRequest[];
groups?: SelectionReadOnlyRequest[];
externalId?: string;
}) {
if (!c.name || !c.name.encryptedString) {
throw new Error("Name not provided for CollectionRequest.");
}
this.name = c.name.encryptedString;
this.externalId = c.externalId;
if (c.groups) {
this.groups = c.groups;
}
if (c.users) {
this.users = c.users;
}
this.name = collection.name ? collection.name.encryptedString : null;
this.externalId = collection.externalId;
}
}

View File

@@ -2,14 +2,14 @@ import { SelectionReadOnlyResponse } from "@bitwarden/common/admin-console/model
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CollectionType } from "./collection";
import { CollectionType, CollectionTypes } from "./collection";
export class CollectionResponse extends BaseResponse {
id: CollectionId;
organizationId: OrganizationId;
name: string;
externalId: string;
type: CollectionType;
externalId: string | undefined;
type: CollectionType = CollectionTypes.SharedCollection;
constructor(response: any) {
super(response);
@@ -17,7 +17,7 @@ export class CollectionResponse extends BaseResponse {
this.organizationId = this.getResponseProperty("OrganizationId");
this.name = this.getResponseProperty("Name");
this.externalId = this.getResponseProperty("ExternalId");
this.type = this.getResponseProperty("Type");
this.type = this.getResponseProperty("Type") ?? CollectionTypes.SharedCollection;
}
}

View File

@@ -1,50 +1,62 @@
import { makeSymmetricCryptoKey, mockEnc } from "@bitwarden/common/spec";
import { MockProxy, mock } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { makeSymmetricCryptoKey } from "@bitwarden/common/spec";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { Collection, CollectionTypes } from "./collection";
import { CollectionData } from "./collection.data";
import { CollectionDetailsResponse } from "./collection.response";
describe("Collection", () => {
let data: CollectionData;
let encService: MockProxy<EncryptService>;
beforeEach(() => {
data = {
id: "id" as CollectionId,
organizationId: "orgId" as OrganizationId,
name: "encName",
externalId: "extId",
readOnly: true,
manage: true,
hidePasswords: true,
type: CollectionTypes.DefaultUserCollection,
};
data = new CollectionData(
new CollectionDetailsResponse({
id: "id" as CollectionId,
organizationId: "orgId" as OrganizationId,
name: "encName",
externalId: "extId",
readOnly: true,
manage: true,
hidePasswords: true,
type: CollectionTypes.DefaultUserCollection,
}),
);
encService = mock<EncryptService>();
encService.decryptString.mockResolvedValue("encName");
});
it("Convert from empty", () => {
const data = new CollectionData({} as any);
const card = new Collection(data);
expect(card).toEqual({
externalId: null,
hidePasswords: null,
id: null,
name: null,
organizationId: null,
readOnly: null,
manage: null,
type: null,
it("Convert from partial", () => {
const card = new Collection({
name: new EncString("name"),
organizationId: "orgId" as OrganizationId,
id: "id" as CollectionId,
});
expect(() => card).not.toThrow();
expect(card.name).not.toBe(null);
expect(card.organizationId).not.toBe(null);
expect(card.id).not.toBe(null);
expect(card.externalId).toBe(undefined);
expect(card.readOnly).toBe(false);
expect(card.manage).toBe(false);
expect(card.hidePasswords).toBe(false);
expect(card.type).toEqual(CollectionTypes.SharedCollection);
});
it("Convert", () => {
const collection = new Collection(data);
const collection = Collection.fromCollectionData(data);
expect(collection).toEqual({
id: "id",
organizationId: "orgId",
name: { encryptedString: "encName", encryptionType: 0 },
externalId: { encryptedString: "extId", encryptionType: 0 },
externalId: "extId",
readOnly: true,
manage: true,
hidePasswords: true,
@@ -53,10 +65,11 @@ describe("Collection", () => {
});
it("Decrypt", async () => {
const collection = new Collection();
collection.id = "id" as CollectionId;
collection.organizationId = "orgId" as OrganizationId;
collection.name = mockEnc("encName");
const collection = new Collection({
name: new EncString("encName"),
organizationId: "orgId" as OrganizationId,
id: "id" as CollectionId,
});
collection.externalId = "extId";
collection.readOnly = false;
collection.hidePasswords = false;
@@ -65,7 +78,7 @@ describe("Collection", () => {
const key = makeSymmetricCryptoKey<OrgKey>();
const view = await collection.decrypt(key);
const view = await collection.decrypt(key, encService);
expect(view).toEqual({
externalId: "extId",

View File

@@ -1,5 +1,6 @@
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base";
import Domain from "@bitwarden/common/platform/models/domain/domain-base";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
@@ -14,45 +15,63 @@ export const CollectionTypes = {
export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes];
export class Collection extends Domain {
id: CollectionId | undefined;
organizationId: OrganizationId | undefined;
name: EncString | undefined;
id: CollectionId;
organizationId: OrganizationId;
name: EncString;
externalId: string | undefined;
readOnly: boolean = false;
hidePasswords: boolean = false;
manage: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
constructor(obj?: CollectionData | null) {
constructor(c: { id: CollectionId; name: EncString; organizationId: OrganizationId }) {
super();
if (obj == null) {
return;
this.id = c.id;
this.name = c.name;
this.organizationId = c.organizationId;
}
static fromCollectionData(obj: CollectionData): Collection {
if (obj == null || obj.name == null || obj.organizationId == null) {
throw new Error("CollectionData must contain name and organizationId.");
}
this.buildDomainModel(
this,
obj,
{
id: null,
organizationId: null,
name: null,
externalId: null,
readOnly: null,
hidePasswords: null,
manage: null,
type: null,
},
["id", "organizationId", "readOnly", "hidePasswords", "manage", "type"],
const collection = new Collection({
...obj,
name: new EncString(obj.name),
});
collection.externalId = obj.externalId;
collection.readOnly = obj.readOnly;
collection.hidePasswords = obj.hidePasswords;
collection.manage = obj.manage;
collection.type = obj.type;
return collection;
}
static async fromCollectionView(
view: CollectionView,
encryptService: EncryptService,
orgKey: OrgKey,
): Promise<Collection> {
return Object.assign(
new Collection({
name: await encryptService.encryptString(view.name, orgKey),
id: view.id,
organizationId: view.organizationId,
}),
view,
);
}
decrypt(orgKey: OrgKey): Promise<CollectionView> {
return this.decryptObj<Collection, CollectionView>(
this,
new CollectionView(this),
["name"] as EncryptableKeys<Collection, CollectionView>[],
this.organizationId ?? null,
orgKey,
);
decrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<CollectionView> {
return CollectionView.fromCollection(this, encryptService, orgKey);
}
// @TODO: This would be better off in Collection.Utils. Move this there when
// refactoring to a shared lib.
static isCollectionId(id: any): id is CollectionId {
return typeof id === "string" && id != null;
}
}

View File

@@ -1,8 +1,11 @@
import { Jsonify } from "type-fest";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { View } from "@bitwarden/common/models/view/view";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
import { Collection, CollectionType, CollectionTypes } from "./collection";
@@ -11,9 +14,9 @@ import { CollectionAccessDetailsResponse } from "./collection.response";
export const NestingDelimiter = "/";
export class CollectionView implements View, ITreeNodeObject {
id: CollectionId | undefined;
organizationId: OrganizationId | undefined;
name: string = "";
id: CollectionId;
organizationId: OrganizationId;
name: string;
externalId: string | undefined;
// readOnly applies to the items within a collection
readOnly: boolean = false;
@@ -22,24 +25,10 @@ export class CollectionView implements View, ITreeNodeObject {
assigned: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
constructor(c?: Collection | CollectionAccessDetailsResponse) {
if (!c) {
return;
}
constructor(c: { id: CollectionId; organizationId: OrganizationId; name: string }) {
this.id = c.id;
this.organizationId = c.organizationId;
this.externalId = c.externalId;
if (c instanceof Collection) {
this.readOnly = c.readOnly;
this.hidePasswords = c.hidePasswords;
this.manage = c.manage;
this.assigned = true;
}
if (c instanceof CollectionAccessDetailsResponse) {
this.assigned = c.assigned;
}
this.type = c.type;
this.name = c.name;
}
canEditItems(org: Organization): boolean {
@@ -94,11 +83,53 @@ export class CollectionView implements View, ITreeNodeObject {
return false;
}
static fromJSON(obj: Jsonify<CollectionView>) {
return Object.assign(new CollectionView(new Collection()), obj);
}
get isDefaultCollection() {
return this.type == CollectionTypes.DefaultUserCollection;
}
// FIXME: we should not use a CollectionView object for the vault filter header because it is not a real
// CollectionView and this violates ts-strict rules.
static vaultFilterHead(): CollectionView {
return new CollectionView({
id: "" as CollectionId,
organizationId: "" as OrganizationId,
name: "",
});
}
static async fromCollection(
collection: Collection,
encryptService: EncryptService,
key: OrgKey,
): Promise<CollectionView> {
const view: CollectionView = Object.assign(
new CollectionView({ ...collection, name: "" }),
collection,
);
view.name = await encryptService.decryptString(collection.name, key);
view.assigned = true;
return view;
}
static async fromCollectionAccessDetails(
collection: CollectionAccessDetailsResponse,
encryptService: EncryptService,
orgKey: OrgKey,
): Promise<CollectionView> {
const view = new CollectionView({ ...collection });
view.name = await encryptService.decryptString(new EncString(collection.name), orgKey);
view.externalId = collection.externalId;
view.type = collection.type;
view.assigned = collection.assigned;
return view;
}
static fromJSON(obj: Jsonify<CollectionView>) {
return Object.assign(new CollectionView({ ...obj }), obj);
}
encrypt(orgKey: OrgKey, encryptService: EncryptService): Promise<Collection> {
return Collection.fromCollectionView(this, encryptService, orgKey);
}
}

View File

@@ -9,13 +9,14 @@ import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionData, CollectionView } from "../models";
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<
CollectionData | null,
CollectionId
>(COLLECTION_DISK, "collections", {
deserializer: (jsonData: Jsonify<CollectionData | null>) => CollectionData.fromJSON(jsonData),
clearOn: ["logout"],
});
export const ENCRYPTED_COLLECTION_DATA_KEY = UserKeyDefinition.record<CollectionData, CollectionId>(
COLLECTION_DISK,
"collections",
{
deserializer: (jsonData: Jsonify<CollectionData>) => CollectionData.fromJSON(jsonData),
clearOn: ["logout"],
},
);
export const DECRYPTED_COLLECTION_DATA_KEY = new UserKeyDefinition<CollectionView[] | null>(
COLLECTION_MEMORY,

View File

@@ -1,12 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { combineLatest, firstValueFrom, from, map, Observable, of, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
@@ -36,12 +32,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
this.keyService.orgKeys$(userId),
from(this.apiService.getManyCollectionsWithAccessDetails(organizationId)),
]).pipe(
switchMap(([orgKey, res]) => {
switchMap(([orgKeys, res]) => {
if (res?.data == null || res.data.length === 0) {
return of([]);
}
if (orgKeys == null) {
throw new Error("No org keys found.");
}
return this.decryptMany(organizationId, res.data, orgKey);
return this.decryptMany(organizationId, res.data, orgKeys);
}),
);
}
@@ -104,55 +103,65 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
orgKeys: Record<OrganizationId, OrgKey>,
): Promise<CollectionAdminView[]> {
const promises = collections.map(async (c) => {
const view = new CollectionAdminView();
view.id = c.id;
view.name = await this.encryptService.decryptString(
new EncString(c.name),
orgKeys[organizationId as OrganizationId],
);
view.externalId = c.externalId;
view.organizationId = c.organizationId;
if (isCollectionAccessDetailsResponse(c)) {
view.groups = c.groups;
view.users = c.users;
view.assigned = c.assigned;
view.readOnly = c.readOnly;
view.hidePasswords = c.hidePasswords;
view.manage = c.manage;
view.unmanaged = c.unmanaged;
return CollectionAdminView.fromCollectionAccessDetails(
c,
this.encryptService,
orgKeys[organizationId as OrganizationId],
);
}
return view;
return await CollectionAdminView.fromCollectionResponse(
c,
this.encryptService,
orgKeys[organizationId as OrganizationId],
);
});
return await Promise.all(promises);
}
private async encrypt(model: CollectionAdminView, userId: UserId): Promise<CollectionRequest> {
if (model.organizationId == null) {
if (!model.organizationId) {
throw new Error("Collection has no organization id.");
}
const key = await firstValueFrom(
this.keyService
.orgKeys$(userId)
.pipe(map((orgKeys) => orgKeys[model.organizationId] ?? null)),
this.keyService.orgKeys$(userId).pipe(
map((orgKeys) => {
if (!orgKeys) {
throw new Error("No keys for the provided userId.");
}
const key = orgKeys[model.organizationId];
if (key == null) {
throw new Error("No key for this collection's organization.");
}
return key;
}),
),
);
if (key == null) {
throw new Error("No key for this collection's organization.");
}
const collection = new CollectionRequest();
collection.externalId = model.externalId;
collection.name = (await this.encryptService.encryptString(model.name, key)).encryptedString;
collection.groups = model.groups.map(
const groups = model.groups.map(
(group) =>
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage),
);
collection.users = model.users.map(
const users = model.users.map(
(user) =>
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage),
);
return collection;
const collectionRequest = new CollectionRequest({
name: await this.encryptService.encryptString(model.name, key),
externalId: model.externalId,
users,
groups,
});
return collectionRequest;
}
}

View File

@@ -390,9 +390,11 @@ const collectionDataFactory = (orgId?: OrganizationId) => {
};
function collectionViewDataFactory(orgId?: OrganizationId): CollectionView {
const collectionView = new CollectionView();
collectionView.id = Utils.newGuid() as CollectionId;
collectionView.organizationId = orgId ?? (Utils.newGuid() as OrganizationId);
collectionView.name = "DEC_NAME_" + collectionView.id;
const id = Utils.newGuid() as CollectionId;
const collectionView = new CollectionView({
id,
organizationId: orgId ?? (Utils.newGuid() as OrganizationId),
name: "DEC_NAME_" + id,
});
return collectionView;
}

View File

@@ -42,9 +42,7 @@ export class DefaultCollectionService implements CollectionService {
/**
* @returns a SingleUserState for encrypted collection data.
*/
private encryptedState(
userId: UserId,
): SingleUserState<Record<CollectionId, CollectionData | null>> {
private encryptedState(userId: UserId): SingleUserState<Record<CollectionId, CollectionData>> {
return this.stateProvider.getUser(userId, ENCRYPTED_COLLECTION_DATA_KEY);
}
@@ -62,7 +60,7 @@ export class DefaultCollectionService implements CollectionService {
return null;
}
return Object.values(collections).map((c) => new Collection(c));
return Object.values(collections).map((c) => Collection.fromCollectionData(c));
}),
);
}
@@ -110,8 +108,8 @@ export class DefaultCollectionService implements CollectionService {
if (collections == null) {
collections = {};
}
collections[toUpdate.id] = toUpdate;
collections[toUpdate.id] = toUpdate;
return collections;
});
@@ -121,7 +119,7 @@ export class DefaultCollectionService implements CollectionService {
if (!orgKeys) {
throw new Error("No key for this collection's organization.");
}
return this.decryptMany$([new Collection(toUpdate)], orgKeys);
return this.decryptMany$([Collection.fromCollectionData(toUpdate)], orgKeys);
}),
),
);
@@ -177,10 +175,6 @@ export class DefaultCollectionService implements CollectionService {
}
async encrypt(model: CollectionView, userId: UserId): Promise<Collection> {
if (model.organizationId == null) {
throw new Error("Collection has no organization id.");
}
const key = await firstValueFrom(
this.keyService.orgKeys$(userId).pipe(
filter((orgKeys) => !!orgKeys),
@@ -188,13 +182,7 @@ export class DefaultCollectionService implements CollectionService {
),
);
const collection = new Collection();
collection.id = model.id;
collection.organizationId = model.organizationId;
collection.readOnly = model.readOnly;
collection.externalId = model.externalId;
collection.name = await this.encryptService.encryptString(model.name, key);
return collection;
return await model.encrypt(key, this.encryptService);
}
// TODO: this should be private.
@@ -211,7 +199,12 @@ export class DefaultCollectionService implements CollectionService {
collections.forEach((collection) => {
decCollections.push(
from(collection.decrypt(orgKeys[collection.organizationId as OrganizationId])),
from(
collection.decrypt(
orgKeys[collection.organizationId as OrganizationId],
this.encryptService,
),
),
);
});
@@ -223,9 +216,8 @@ export class DefaultCollectionService implements CollectionService {
getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[] {
const nodes: TreeNode<CollectionView>[] = [];
collections.forEach((c) => {
const collectionCopy = new CollectionView();
collectionCopy.id = c.id;
collectionCopy.organizationId = c.organizationId;
const collectionCopy = Object.assign(new CollectionView({ ...c }), c);
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);
});

View File

@@ -3,20 +3,18 @@
// 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
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { CollectionId } from "../../types/guid";
import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionExport } from "./collection.export";
export class CollectionWithIdExport extends CollectionExport {
id: CollectionId;
static toView(req: CollectionWithIdExport, view = new CollectionView()) {
view.id = req.id;
return super.toView(req, view);
static toView(req: CollectionWithIdExport) {
return super.toView(req, req.id);
}
static toDomain(req: CollectionWithIdExport, domain = new CollectionDomain()) {
static toDomain(req: CollectionWithIdExport, domain: CollectionDomain) {
domain.id = req.id;
return super.toDomain(req, domain);
}

View File

@@ -5,7 +5,7 @@
import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { emptyGuid, OrganizationId } from "../../types/guid";
import { CollectionId, emptyGuid, OrganizationId } from "../../types/guid";
import { safeGetString } from "./utils";
@@ -18,16 +18,17 @@ export class CollectionExport {
return req;
}
static toView(req: CollectionExport, view = new CollectionView()) {
view.name = req.name;
static toView(req: CollectionExport, id: CollectionId) {
const view = new CollectionView({
name: req.name,
organizationId: req.organizationId,
id,
});
view.externalId = req.externalId;
if (view.organizationId == null) {
view.organizationId = req.organizationId;
}
return view;
}
static toDomain(req: CollectionExport, domain = new CollectionDomain()) {
static toDomain(req: CollectionExport, domain: CollectionDomain) {
domain.name = req.name != null ? new EncString(req.name) : null;
domain.externalId = req.externalId;
if (domain.organizationId == null) {

View File

@@ -13,8 +13,8 @@ export const getById = <TId, T extends { id: TId }>(id: TId) =>
* @param id The IDs of the objects to return.
* @returns An array containing objects with matching IDs, or an empty array if there are no matching objects.
*/
export const getByIds = <TId, T extends { id: TId | undefined }>(ids: TId[]) => {
const idSet = new Set(ids.filter((id) => id != null));
export const getByIds = <TId, T extends { id: TId }>(ids: TId[]) => {
const idSet = new Set(ids);
return map<T[], T[]>((objects) => {
return objects.filter((o) => o.id && idSet.has(o.id));
});

View File

@@ -13,7 +13,7 @@ export type DecryptedObject<
> = Record<TDecryptedKeys, string> & Omit<TEncryptedObject, TDecryptedKeys>;
// extracts shared keys from the domain and view types
export type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
type EncryptableKeys<D extends Domain, V extends View> = (keyof D &
ConditionalKeys<D, EncString | null>) &
(keyof V & ConditionalKeys<V, string | null>);

View File

@@ -4,12 +4,12 @@ import * as papa from "papaparse";
// 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
import { CollectionView } from "@bitwarden/admin-console/common";
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@@ -278,9 +278,12 @@ export abstract class BaseImporter {
protected moveFoldersToCollections(result: ImportResult) {
result.folderRelationships.forEach((r) => result.collectionRelationships.push(r));
result.collections = result.folders.map((f) => {
const collection = new CollectionView();
collection.name = f.name;
collection.id = (f.id as CollectionId) ?? undefined; // folder id may be null, which is not suitable for collections.
const collection = new CollectionView({
name: f.name,
organizationId: this.organizationId,
// FIXME: Folder.id may be null, this should be changed when refactoring Folders to be ts-strict
id: Collection.isCollectionId(f.id) ? f.id : null,
});
return collection;
});
result.folderRelationships = [];

View File

@@ -1,10 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { concatMap, firstValueFrom, map } from "rxjs";
// 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
import { CollectionView } from "@bitwarden/admin-console/common";
import { Collection, CollectionView } from "@bitwarden/admin-console/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
@@ -206,11 +206,20 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
for (const c of data.collections) {
let collectionView: CollectionView;
if (data.encrypted) {
const collection = CollectionWithIdExport.toDomain(c);
collection.organizationId = this.organizationId;
collectionView = await firstValueFrom(this.keyService.activeUserOrgKeys$).then((orgKeys) =>
collection.decrypt(orgKeys[c.organizationId as OrganizationId]),
const collection = CollectionWithIdExport.toDomain(
c,
new Collection({
id: c.id,
name: new EncString(c.name),
organizationId: this.organizationId,
}),
);
const collection$ = this.keyService.activeUserOrgKeys$.pipe(
// FIXME: replace type assertion with narrowing
map((keys) => keys[c.organizationId as OrganizationId]),
concatMap((key) => collection.decrypt(key, this.encryptService)),
);
collectionView = await firstValueFrom(collection$);
} else {
collectionView = CollectionWithIdExport.toView(c);
collectionView.organizationId = null;

View File

@@ -46,8 +46,11 @@ export class PadlockCsvImporter extends BaseImporter implements Importer {
}
if (addCollection) {
const collection = new CollectionView();
collection.name = tag;
// FIXME use a different model if ID is not required.
// @ts-expect-error current functionality creates this view with no Id since its being imported.
const collection = new CollectionView({
name: tag,
});
result.collections.push(collection);
}

View File

@@ -47,8 +47,11 @@ export class PasspackCsvImporter extends BaseImporter implements Importer {
}
if (addCollection) {
const collection = new CollectionView();
collection.name = tag;
// FIXME use a different model if ID is not required.
// @ts-expect-error current functionality creates this view with no Id since its being imported.
const collection = new CollectionView({
name: tag,
});
result.collections.push(collection);
}

View File

@@ -487,8 +487,11 @@ describe("Password Depot 17 Xml Importer", () => {
it("should parse groups nodes into collections when importing into an organization", async () => {
const importer = new PasswordDepot17XmlImporter();
importer.organizationId = "someOrgId" as OrganizationId;
const collection = new CollectionView();
collection.name = "tempDB";
const collection = new CollectionView({
name: "tempDB",
organizationId: importer.organizationId,
id: null,
});
const actual = [collection];
const result = await importer.parse(PasswordTestData);

View File

@@ -145,20 +145,29 @@ describe("ImportService", () => {
);
});
const mockImportTargetCollection = new CollectionView();
mockImportTargetCollection.id = "myImportTarget" as CollectionId;
mockImportTargetCollection.name = "myImportTarget";
mockImportTargetCollection.organizationId = organizationId;
const mockName = "myImportTarget";
const mockId = "myImportTarget" as CollectionId;
const mockImportTargetCollection = new CollectionView({
name: mockName,
id: mockId,
organizationId,
});
const mockCollection1 = new CollectionView();
mockCollection1.id = "collection1" as CollectionId;
mockCollection1.name = "collection1";
mockCollection1.organizationId = organizationId;
const mockName1 = "collection1";
const mockId1 = "collection1" as CollectionId;
const mockCollection1 = new CollectionView({
name: mockName1,
id: mockId1,
organizationId,
});
const mockCollection2 = new CollectionView();
mockCollection2.id = "collection2" as CollectionId;
mockCollection2.name = "collection2";
mockCollection2.organizationId = organizationId;
const mockName2 = "collection2";
const mockId2 = "collection2" as CollectionId;
const mockCollection2 = new CollectionView({
name: mockName2,
id: mockId2,
organizationId,
});
it("passing importTarget adds it to collections", async () => {
await importService["setImportTarget"](

View File

@@ -501,7 +501,7 @@ export class ImportService implements ImportServiceAbstraction {
const collections: CollectionView[] = [...importResult.collections];
importResult.collections = [importTarget as CollectionView];
collections.map((x) => {
const f = new CollectionView();
const f = new CollectionView(x);
f.name = `${importTarget.name}/${x.name}`;
importResult.collections.push(f);
});

View File

@@ -143,10 +143,14 @@ export class OrganizationVaultExportService
if (exportData != null) {
if (exportData.collections != null && exportData.collections.length > 0) {
exportData.collections.forEach((c) => {
const collection = new Collection(new CollectionData(c as CollectionDetailsResponse));
const collection = Collection.fromCollectionData(
new CollectionData(c as CollectionDetailsResponse),
);
exportPromises.push(
firstValueFrom(this.keyService.activeUserOrgKeys$)
.then((keys) => collection.decrypt(keys[organizationId as OrganizationId]))
.then((keys) =>
collection.decrypt(keys[organizationId as OrganizationId], this.encryptService),
)
.then((decCol) => {
decCollections.push(decCol);
}),
@@ -191,7 +195,9 @@ export class OrganizationVaultExportService
this.apiService.getCollections(organizationId).then((c) => {
if (c != null && c.data != null && c.data.length > 0) {
c.data.forEach((r) => {
const collection = new Collection(new CollectionData(r as CollectionDetailsResponse));
const collection = Collection.fromCollectionData(
new CollectionData(r as CollectionDetailsResponse),
);
collections.push(collection);
});
}

View File

@@ -48,6 +48,7 @@ const createMockCollection = (
canEdit: jest.fn(),
canDelete: jest.fn(),
canViewCollectionInfo: jest.fn(),
encrypt: jest.fn(),
};
};

View File

@@ -29,23 +29,27 @@ describe("AssignCollectionsComponent", () => {
const mockUserId = "mock-user-id" as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const editCollection = new CollectionView();
editCollection.id = "collection-id" as CollectionId;
editCollection.organizationId = "org-id" as OrganizationId;
editCollection.name = "Editable Collection";
const editCollection = new CollectionView({
id: "collection-id" as CollectionId,
organizationId: "org-id" as OrganizationId,
name: "Editable Collection",
});
editCollection.readOnly = false;
editCollection.manage = true;
const readOnlyCollection1 = new CollectionView();
readOnlyCollection1.id = "read-only-collection-id" as CollectionId;
readOnlyCollection1.organizationId = "org-id" as OrganizationId;
readOnlyCollection1.name = "Read Only Collection";
const readOnlyCollection1 = new CollectionView({
id: "read-only-collection-id" as CollectionId,
organizationId: "org-id" as OrganizationId,
name: "Read Only Collection",
});
readOnlyCollection1.readOnly = true;
const readOnlyCollection2 = new CollectionView();
readOnlyCollection2.id = "read-only-collection-id-2" as CollectionId;
readOnlyCollection2.organizationId = "org-id" as OrganizationId;
readOnlyCollection2.name = "Read Only Collection 2";
const readOnlyCollection2 = new CollectionView({
id: "read-only-collection-id-2" as CollectionId,
organizationId: "org-id" as OrganizationId,
name: "Read Only Collection 2",
});
readOnlyCollection2.readOnly = true;
const params = {