1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-05 18:13:26 +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

@@ -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);
});