1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-22717] Expose DefaultUserCollectionEmail to clients (#15643)

* enforce restrictions based on collection type, set default collection type

* fix ts strict errors

* fix default collection enforcement in vault header

* enforce default collection restrictions in vault collection row

* enforce default collection restrictions in AC vault header

* enforce default collection restriction for select all

* fix ts strict error

* switch to signal, fix feature flag

* fix story

* clean up

* remove feature flag, move check for defaultCollecion to CollecitonView

* fix test

* remove unused configService

* fix test: coerce null to undefined for collection Id

* clean up leaky abstraction for default collection

* fix ts-strict error

* fix parens

* add new property to models, update logic, refactor for ts-strict

* fix type

* rename defaultCollection getter

* clean up

* clean up

* clean up, add comment, fix submit

* add comment

* add feature flag

* check model for name

* cleanup readonly logic, remove featureflag logic

* wip

* refactor CollectionRequest into Create and Update models

* fix readonly logic

* cleanup

* set defaultUserCollectionEmail in decryption from Collection

* split save into update/create methods

* fix readonly logic

* fix collections post and put requests

* add defaultUserCollection email to model when submitting collection dialog
This commit is contained in:
Brandon Treston
2025-08-26 11:42:52 -04:00
committed by GitHub
parent ad2dfe1e99
commit 28b5a2bb5e
20 changed files with 248 additions and 79 deletions

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom, map, switchMap } from "rxjs";
import { CollectionRequest } from "@bitwarden/admin-console/common";
import { UpdateCollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -225,7 +225,7 @@ export class EditCommand {
: req.users.map(
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
);
const request = new CollectionRequest({
const request = new UpdateCollectionRequest({
name: await this.encryptService.encryptString(req.name, orgKey),
externalId: req.externalId,
users,

View File

@@ -5,7 +5,7 @@ import * as path from "path";
import { firstValueFrom, map } from "rxjs";
import { CollectionRequest } from "@bitwarden/admin-console/common";
import { CreateCollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
@@ -233,7 +233,7 @@ export class CreateCommand {
: req.users.map(
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
);
const request = new CollectionRequest({
const request = new CreateCollectionRequest({
name: await this.encryptService.encryptString(req.name, orgKey),
externalId: req.externalId,
groups,

View File

@@ -45,9 +45,15 @@ export function cloneCollection(
let cloned;
if (collection instanceof CollectionAdminView) {
cloned = Object.assign(new CollectionAdminView({ ...collection }), collection);
cloned = Object.assign(
new CollectionAdminView({ ...collection, name: collection.name }),
collection,
);
} else {
cloned = Object.assign(new CollectionView({ ...collection }), collection);
cloned = Object.assign(
new CollectionView({ ...collection, name: collection.name }),
collection,
);
}
return cloned;
}

View File

@@ -398,6 +398,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
}
return;
}
if (
this.editMode &&
!this.collection.canEditName(this.organization) &&
this.formGroup.controls.name.dirty
) {
throw new Error("Cannot change readonly field: Name");
}
const parent = this.formGroup.controls.parent?.value;
const collectionView = new CollectionAdminView({
@@ -414,9 +421,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
collectionView.users = this.formGroup.controls.access.value
.filter((v) => v.type === AccessItemType.Member)
.map(convertToSelectionView);
collectionView.defaultUserCollectionEmail = this.collection.defaultUserCollectionEmail;
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const savedCollection = await this.collectionAdminService.save(collectionView, userId);
const collectionResponse = this.editMode
? await this.collectionAdminService.update(collectionView, userId)
: await this.collectionAdminService.create(collectionView, userId);
this.toastService.showToast({
variant: "success",
@@ -426,7 +437,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
),
});
this.close(CollectionDialogAction.Saved, savedCollection);
this.close(CollectionDialogAction.Saved, collectionResponse);
};
protected delete = async () => {
@@ -483,14 +494,23 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private handleFormGroupReadonly(readonly: boolean) {
if (readonly) {
this.formGroup.controls.access.disable();
this.formGroup.controls.name.disable();
this.formGroup.controls.parent.disable();
this.formGroup.controls.access.disable();
} else {
return;
}
this.formGroup.controls.access.enable();
if (!this.editMode) {
this.formGroup.controls.name.enable();
this.formGroup.controls.parent.enable();
this.formGroup.controls.access.enable();
return;
}
const canEditName = this.collection.canEditName(this.organization);
this.formGroup.controls.name[canEditName ? "enable" : "disable"]();
this.formGroup.controls.parent[canEditName ? "enable" : "disable"]();
}
private close(action: CollectionDialogAction, collection?: CollectionResponse | CollectionView) {

View File

@@ -45,6 +45,7 @@ import {
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import {
InternalPolicyService,
@@ -317,7 +318,13 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: CollectionAdminService,
useClass: DefaultCollectionAdminService,
deps: [ApiService, KeyServiceAbstraction, EncryptService, CollectionService],
deps: [
ApiService,
KeyServiceAbstraction,
EncryptService,
CollectionService,
OrganizationService,
],
}),
safeProvider({
provide: SdkLoadService,

View File

@@ -250,7 +250,9 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}
collections.forEach((c) => {
const collectionCopy = cloneCollection(new CollectionView({ ...c })) as CollectionFilter;
const collectionCopy = cloneCollection(
new CollectionView({ ...c, name: c.name }),
) as CollectionFilter;
collectionCopy.icon = "bwi-collection-shared";
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, null, NestingDelimiter);

View File

@@ -10,7 +10,11 @@ export abstract class CollectionAdminService {
organizationId: string,
userId: UserId,
): Observable<CollectionAdminView[]>;
abstract save(
abstract update(
collection: CollectionAdminView,
userId: UserId,
): Promise<CollectionDetailsResponse>;
abstract create(
collection: CollectionAdminView,
userId: UserId,
): Promise<CollectionDetailsResponse>;

View File

@@ -101,6 +101,17 @@ export class CollectionAdminView extends CollectionView {
return this.id === Unassigned;
}
/**
* Returns true if the collection name can be edited. Editing the collection name is restricted for collections
* that were DefaultUserCollections but where the relevant user has been offboarded.
* When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited.
* This is important for security so that the server cannot ask the client to encrypt arbitrary data.
* WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes.
* Do not edit or remove this unless you understand why.
*/
override canEditName(org: Organization): boolean {
return (this.canEdit(org) && !this.defaultUserCollectionEmail) || super.canEditName(org);
}
static async fromCollectionAccessDetails(
collection: CollectionAccessDetailsResponse,
encryptService: EncryptService,
@@ -115,6 +126,7 @@ export class CollectionAdminView extends CollectionView {
view.unmanaged = collection.unmanaged;
view.type = collection.type;
view.externalId = collection.externalId;
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
view.groups = collection.groups
? collection.groups.map((g) => new CollectionAccessSelectionView(g))

View File

@@ -1,19 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Collection } from "./collection";
import { CollectionRequest } from "./collection.request";
import { BaseCollectionRequest } from "./collection.request";
export class CollectionWithIdRequest extends CollectionRequest {
export class CollectionWithIdRequest extends BaseCollectionRequest {
id: string;
name: string;
constructor(collection?: Collection) {
if (collection == null) {
return;
constructor(collection: Collection) {
if (collection == null || collection.name == null || collection.name.encryptedString == null) {
throw new Error("CollectionWithIdRequest must contain name.");
}
super({
name: collection.name,
externalId: collection.externalId,
});
this.name = collection.name.encryptedString;
this.id = collection.id;
}
}

View File

@@ -9,6 +9,7 @@ export class CollectionData {
id: CollectionId;
organizationId: OrganizationId;
name: string;
defaultUserCollectionEmail: string | undefined;
externalId: string | undefined;
readOnly: boolean = false;
manage: boolean = false;
@@ -24,6 +25,7 @@ export class CollectionData {
this.manage = response.manage;
this.hidePasswords = response.hidePasswords;
this.type = response.type;
this.defaultUserCollectionEmail = response.defaultUserCollectionEmail;
}
static fromJSON(obj: Jsonify<CollectionData | null>): CollectionData | null {

View File

@@ -1,23 +1,20 @@
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
export class CollectionRequest {
name: string;
export abstract class BaseCollectionRequest {
externalId: string | undefined;
groups: SelectionReadOnlyRequest[] = [];
users: SelectionReadOnlyRequest[] = [];
constructor(c: {
name: EncString;
static isUpdate = (request: BaseCollectionRequest): request is UpdateCollectionRequest => {
return request instanceof UpdateCollectionRequest;
};
protected constructor(c: {
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) {
@@ -28,3 +25,36 @@ export class CollectionRequest {
}
}
}
export class CreateCollectionRequest extends BaseCollectionRequest {
name: string;
constructor(c: {
name: EncString;
users?: SelectionReadOnlyRequest[];
groups?: SelectionReadOnlyRequest[];
externalId?: string;
}) {
super(c);
if (!c.name || !c.name.encryptedString) {
throw new Error("Name not provided for CollectionRequest.");
}
this.name = c.name.encryptedString;
}
}
export class UpdateCollectionRequest extends BaseCollectionRequest {
name: string | null;
constructor(c: {
name: EncString | null;
users?: SelectionReadOnlyRequest[];
groups?: SelectionReadOnlyRequest[];
externalId?: string;
}) {
super(c);
this.name = c.name?.encryptedString ?? null;
}
}

View File

@@ -8,6 +8,7 @@ export class CollectionResponse extends BaseResponse {
id: CollectionId;
organizationId: OrganizationId;
name: string;
defaultUserCollectionEmail: string | undefined;
externalId: string | undefined;
type: CollectionType = CollectionTypes.SharedCollection;
@@ -17,6 +18,7 @@ export class CollectionResponse extends BaseResponse {
this.organizationId = this.getResponseProperty("OrganizationId");
this.name = this.getResponseProperty("Name");
this.externalId = this.getResponseProperty("ExternalId");
this.defaultUserCollectionEmail = this.getResponseProperty("DefaultUserCollectionEmail");
this.type = this.getResponseProperty("Type") ?? CollectionTypes.SharedCollection;
}
}

View File

@@ -25,6 +25,7 @@ describe("Collection", () => {
manage: true,
hidePasswords: true,
type: CollectionTypes.DefaultUserCollection,
defaultUserCollectionEmail: "defaultCollectionEmail",
}),
);
encService = mock<EncryptService>();
@@ -61,6 +62,7 @@ describe("Collection", () => {
manage: true,
hidePasswords: true,
type: CollectionTypes.DefaultUserCollection,
defaultUserCollectionEmail: "defaultCollectionEmail",
});
});
@@ -75,6 +77,7 @@ describe("Collection", () => {
collection.hidePasswords = false;
collection.manage = true;
collection.type = CollectionTypes.DefaultUserCollection;
collection.defaultUserCollectionEmail = "defaultCollectionEmail";
const key = makeSymmetricCryptoKey<OrgKey>();
@@ -84,12 +87,13 @@ describe("Collection", () => {
externalId: "extId",
hidePasswords: false,
id: "id",
name: "encName",
_name: "encName",
organizationId: "orgId",
readOnly: false,
manage: true,
assigned: true,
type: CollectionTypes.DefaultUserCollection,
defaultUserCollectionEmail: "defaultCollectionEmail",
});
});
});

View File

@@ -23,6 +23,7 @@ export class Collection extends Domain {
hidePasswords: boolean = false;
manage: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
defaultUserCollectionEmail: string | undefined;
constructor(c: { id: CollectionId; name: EncString; organizationId: OrganizationId }) {
super();
@@ -46,6 +47,7 @@ export class Collection extends Domain {
collection.hidePasswords = obj.hidePasswords;
collection.manage = obj.manage;
collection.type = obj.type;
collection.defaultUserCollectionEmail = obj.defaultUserCollectionEmail;
return collection;
}

View File

@@ -16,7 +16,6 @@ export const NestingDelimiter = "/";
export class CollectionView implements View, ITreeNodeObject {
id: CollectionId;
organizationId: OrganizationId;
name: string;
externalId: string | undefined;
// readOnly applies to the items within a collection
readOnly: boolean = false;
@@ -24,11 +23,22 @@ export class CollectionView implements View, ITreeNodeObject {
manage: boolean = false;
assigned: boolean = false;
type: CollectionType = CollectionTypes.SharedCollection;
defaultUserCollectionEmail: string | undefined;
private _name: string;
constructor(c: { id: CollectionId; organizationId: OrganizationId; name: string }) {
this.id = c.id;
this.organizationId = c.organizationId;
this.name = c.name;
this._name = c.name;
}
set name(name: string) {
this._name = name;
}
get name(): string {
return this.defaultUserCollectionEmail ?? this._name;
}
canEditItems(org: Organization): boolean {
@@ -83,6 +93,18 @@ export class CollectionView implements View, ITreeNodeObject {
return false;
}
/**
* Returns true if the collection name can be edited. Editing the collection name is restricted for collections
* that were DefaultUserCollections but where the relevant user has been offboarded.
* When this occurs, the offboarded user's email is treated as the collection name, and cannot be edited.
* This is important for security so that the server cannot ask the client to encrypt arbitrary data.
* WARNING! This is an IMPORTANT restriction that MUST be maintained for security purposes.
* Do not edit or remove this unless you understand why.
*/
canEditName(org: Organization): boolean {
return this.canEdit(org) && !this.defaultUserCollectionEmail;
}
get isDefaultCollection() {
return this.type == CollectionTypes.DefaultUserCollection;
}
@@ -111,6 +133,7 @@ export class CollectionView implements View, ITreeNodeObject {
view.hidePasswords = collection.hidePasswords;
view.manage = collection.manage;
view.type = collection.type;
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
return view;
}
@@ -125,6 +148,7 @@ export class CollectionView implements View, ITreeNodeObject {
view.externalId = collection.externalId;
view.type = collection.type;
view.assigned = collection.assigned;
view.defaultUserCollectionEmail = collection.defaultUserCollectionEmail;
return view;
}

View File

@@ -1,6 +1,10 @@
import { combineLatest, firstValueFrom, from, map, Observable, of, switchMap } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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 { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
@@ -10,13 +14,15 @@ import { KeyService } from "@bitwarden/key-management";
import { CollectionAdminService, CollectionService } from "../abstractions";
import {
CollectionData,
CollectionRequest,
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionResponse,
BulkCollectionAccessRequest,
CollectionAccessSelectionView,
CollectionAdminView,
BaseCollectionRequest,
UpdateCollectionRequest,
CreateCollectionRequest,
} from "../models";
export class DefaultCollectionAdminService implements CollectionAdminService {
@@ -25,6 +31,7 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
private keyService: KeyService,
private encryptService: EncryptService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
) {}
collectionAdminViews$(organizationId: string, userId: UserId): Observable<CollectionAdminView[]> {
@@ -45,27 +52,40 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
);
}
async save(collection: CollectionAdminView, userId: UserId): Promise<CollectionDetailsResponse> {
const request = await this.encrypt(collection, userId);
let response: CollectionDetailsResponse;
if (collection.id == null) {
response = await this.apiService.postCollection(collection.organizationId, request);
collection.id = response.id;
} else {
response = await this.apiService.putCollection(
collection.organizationId,
collection.id,
request,
);
async update(
collection: CollectionAdminView,
userId: UserId,
): Promise<CollectionDetailsResponse> {
const request = await this.encrypt(collection, userId, true);
if (!BaseCollectionRequest.isUpdate(request)) {
throw new Error("Cannot update collection with CreateCollectionRequest.");
}
if (response.assigned) {
await this.collectionService.upsert(new CollectionData(response), userId);
} else {
await this.collectionService.delete([collection.id as CollectionId], userId);
const response = await this.apiService.putCollection(
collection.organizationId,
collection.id,
request,
);
await this.updateLocalCollections(response, collection, userId);
return response;
}
async create(
collection: CollectionAdminView,
userId: UserId,
): Promise<CollectionDetailsResponse> {
const request = await this.encrypt(collection, userId, false);
if (BaseCollectionRequest.isUpdate(request)) {
throw new Error("Cannot create collection with UpdateCollectionRequest.");
}
const response = await this.apiService.postCollection(collection.organizationId, request);
collection.id = response.id;
await this.updateLocalCollections(response, collection, userId);
return response;
}
@@ -73,6 +93,16 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
await this.apiService.deleteCollection(organizationId, collectionId);
}
private async updateLocalCollections(
response: CollectionDetailsResponse,
collection: CollectionAdminView,
userId: UserId,
) {
response.assigned
? await this.collectionService.upsert(new CollectionData(response), userId)
: await this.collectionService.delete([collection.id as CollectionId], userId);
}
async bulkAssignAccess(
organizationId: string,
collectionIds: string[],
@@ -118,10 +148,15 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
);
});
return await Promise.all(promises);
const r = await Promise.all(promises);
return r;
}
private async encrypt(model: CollectionAdminView, userId: UserId): Promise<CollectionRequest> {
private async encrypt(
model: CollectionAdminView,
userId: UserId,
editMode: boolean,
): Promise<UpdateCollectionRequest | CreateCollectionRequest> {
if (!model.organizationId) {
throw new Error("Collection has no organization id.");
}
@@ -154,14 +189,31 @@ export class DefaultCollectionAdminService implements CollectionAdminService {
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage),
);
const collectionRequest = new CollectionRequest({
if (editMode) {
const org = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(model.organizationId)),
);
if (org == null) {
throw new Error("No Organization found.");
}
return new UpdateCollectionRequest({
name: model.canEditName(org)
? await this.encryptService.encryptString(model.name, key)
: null,
externalId: model.externalId,
users,
groups,
});
}
return new CreateCollectionRequest({
name: await this.encryptService.encryptString(model.name, key),
externalId: model.externalId,
users,
groups,
});
return collectionRequest;
}
}

View File

@@ -216,7 +216,7 @@ export class DefaultCollectionService implements CollectionService {
getAllNested(collections: CollectionView[]): TreeNode<CollectionView>[] {
const nodes: TreeNode<CollectionView>[] = [];
collections.forEach((c) => {
const collectionCopy = Object.assign(new CollectionView({ ...c }), c);
const collectionCopy = Object.assign(new CollectionView({ ...c, name: c.name }), c);
const parts = c.name != null ? c.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, collectionCopy, undefined, NestingDelimiter);

View File

@@ -3,8 +3,9 @@
import {
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionRequest,
CollectionResponse,
CreateCollectionRequest,
UpdateCollectionRequest,
} from "@bitwarden/admin-console/common";
import { OrganizationConnectionType } from "../admin-console/enums";
@@ -270,12 +271,12 @@ export abstract class ApiService {
): Promise<ListResponse<CollectionAccessDetailsResponse>>;
abstract postCollection(
organizationId: string,
request: CollectionRequest,
request: CreateCollectionRequest,
): Promise<CollectionDetailsResponse>;
abstract putCollection(
organizationId: string,
id: string,
request: CollectionRequest,
request: UpdateCollectionRequest,
): Promise<CollectionDetailsResponse>;
abstract deleteCollection(organizationId: string, id: string): Promise<any>;
abstract deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any>;

View File

@@ -7,8 +7,9 @@ import { firstValueFrom } from "rxjs";
import {
CollectionAccessDetailsResponse,
CollectionDetailsResponse,
CollectionRequest,
CollectionResponse,
CreateCollectionRequest,
UpdateCollectionRequest,
} from "@bitwarden/admin-console/common";
// 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
@@ -727,7 +728,7 @@ export class ApiService implements ApiServiceAbstraction {
async postCollection(
organizationId: string,
request: CollectionRequest,
request: CreateCollectionRequest,
): Promise<CollectionDetailsResponse> {
const r = await this.send(
"POST",
@@ -742,7 +743,7 @@ export class ApiService implements ApiServiceAbstraction {
async putCollection(
organizationId: string,
id: string,
request: CollectionRequest,
request: UpdateCollectionRequest,
): Promise<CollectionDetailsResponse> {
const r = await this.send(
"PUT",

View File

@@ -33,23 +33,24 @@ const createMockCollection = (
readOnly = false,
canEdit = true,
): CollectionView => {
return {
id: id as CollectionId,
const cv = new CollectionView({
name,
organizationId: organizationId as OrganizationId,
externalId: "",
readOnly,
hidePasswords: false,
manage: true,
assigned: true,
type: CollectionTypes.DefaultUserCollection,
isDefaultCollection: true,
canEditItems: jest.fn().mockReturnValue(canEdit),
canEdit: jest.fn(),
canDelete: jest.fn(),
canViewCollectionInfo: jest.fn(),
encrypt: jest.fn(),
};
id: id as CollectionId,
});
cv.readOnly = readOnly;
cv.manage = true;
cv.type = CollectionTypes.DefaultUserCollection;
cv.externalId = "";
cv.hidePasswords = false;
cv.assigned = true;
cv.canEditName = jest.fn().mockReturnValue(true);
cv.canEditItems = jest.fn().mockReturnValue(canEdit);
cv.canEdit = jest.fn();
cv.canDelete = jest.fn();
cv.canViewCollectionInfo = jest.fn();
return cv;
};
describe("ItemDetailsSectionComponent", () => {