diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.html b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.html
index f4248331ff..05a089c5d3 100644
--- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.html
+++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.html
@@ -4,8 +4,8 @@
-
- {{ "deleteSelectedItemsDesc" | i18n: cipherIds.length }}
+
+ {{ "deleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
{{ "deleteSelectedCollectionsDesc" | i18n: collections.length }}
@@ -13,7 +13,7 @@
{{ "deleteSelectedConfirmation" | i18n }}
- {{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length }}
+ {{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts
index ee036f5e3b..c0de8c6bd2 100644
--- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts
+++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts
@@ -20,6 +20,7 @@ export interface BulkDeleteDialogParams {
organization?: Organization;
organizations?: Organization[];
collections?: CollectionView[];
+ unassignedCiphers?: string[];
}
export enum BulkDeleteDialogResult {
@@ -51,6 +52,7 @@ export class BulkDeleteDialogComponent {
organization: Organization;
organizations: Organization[];
collections: CollectionView[];
+ unassignedCiphers: string[];
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
@@ -75,6 +77,7 @@ export class BulkDeleteDialogComponent {
this.organization = params.organization;
this.organizations = params.organizations;
this.collections = params.collections;
+ this.unassignedCiphers = params.unassignedCiphers || [];
}
protected async cancel() {
@@ -83,6 +86,15 @@ export class BulkDeleteDialogComponent {
protected submit = async () => {
const deletePromises: Promise[] = [];
+ const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
+
+ // Unassigned ciphers under an Owner/Admin OR Custom Users With Edit will call the deleteCiphersAdmin method
+ if (
+ this.unassignedCiphers.length &&
+ this.organization.canEditUnassignedCiphers(restrictProviderAccess)
+ ) {
+ deletePromises.push(this.deleteCiphersAdmin(this.unassignedCiphers));
+ }
if (this.cipherIds.length) {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
@@ -93,7 +105,7 @@ export class BulkDeleteDialogComponent {
) {
deletePromises.push(this.deleteCiphers());
} else {
- deletePromises.push(this.deleteCiphersAdmin());
+ deletePromises.push(this.deleteCiphersAdmin(this.cipherIds));
}
}
@@ -103,7 +115,7 @@ export class BulkDeleteDialogComponent {
await Promise.all(deletePromises);
- if (this.cipherIds.length) {
+ if (this.cipherIds.length || this.unassignedCiphers.length) {
this.platformUtilsService.showToast(
"success",
null,
@@ -135,8 +147,8 @@ export class BulkDeleteDialogComponent {
}
}
- private async deleteCiphersAdmin(): Promise {
- const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id);
+ private async deleteCiphersAdmin(ciphers: string[]): Promise {
+ const deleteRequest = new CipherBulkDeleteRequest(ciphers, this.organization.id);
if (this.permanent) {
return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
} else {
diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts
index 2ddc0c116d..0247b89bfd 100644
--- a/apps/web/src/app/vault/org-vault/vault.component.ts
+++ b/apps/web/src/app/vault/org-vault/vault.component.ts
@@ -751,7 +751,7 @@ export class VaultComponent implements OnInit, OnDestroy {
if (ciphers.length === 1 && collections.length === 0) {
await this.deleteCipher(ciphers[0]);
} else if (ciphers.length === 0 && collections.length === 1) {
- await this.deleteCollection(collections[0]);
+ await this.deleteCollection(collections[0] as CollectionAdminView);
} else {
await this.bulkDelete(ciphers, collections, this.organization);
}
@@ -980,6 +980,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
if (
+ !this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled &&
!c.edit &&
!this.organization.allowAdminAccessToAllCollectionItems
@@ -992,8 +993,11 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
+ // Allow restore of an Unassigned Item
try {
- const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled);
+ const asAdmin =
+ this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
+ c.isUnassigned;
await this.cipherService.restoreWithServer(c.id, asAdmin);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh();
@@ -1004,6 +1008,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async bulkRestore(ciphers: CipherView[]) {
if (
+ !this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled &&
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
) {
@@ -1015,13 +1020,46 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
- const selectedCipherIds = ciphers.map((cipher) => cipher.id);
- if (selectedCipherIds.length === 0) {
- this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
+ // assess if there are unassigned ciphers and/or editable ciphers selected in bulk for restore
+ const editAccessCiphers: string[] = [];
+ const unassignedCiphers: string[] = [];
+
+ // If user has edit all Access no need to check for unassigned ciphers
+ const canEditAll = this.organization.canEditAllCiphers(
+ this.flexibleCollectionsV1Enabled,
+ this.restrictProviderAccessEnabled,
+ );
+
+ if (canEditAll) {
+ ciphers.map((cipher) => {
+ editAccessCiphers.push(cipher.id);
+ });
+ } else {
+ ciphers.map((cipher) => {
+ if (cipher.collectionIds.length === 0) {
+ unassignedCiphers.push(cipher.id);
+ } else if (cipher.edit) {
+ editAccessCiphers.push(cipher.id);
+ }
+ });
+ }
+
+ if (unassignedCiphers.length === 0 && editAccessCiphers.length === 0) {
+ this.platformUtilsService.showToast(
+ "error",
+ this.i18nService.t("errorOccurred"),
+ this.i18nService.t("nothingSelected"),
+ );
return;
}
- await this.cipherService.restoreManyWithServer(selectedCipherIds);
+ if (unassignedCiphers.length > 0 || editAccessCiphers.length > 0) {
+ await this.cipherService.restoreManyWithServer(
+ [...unassignedCiphers, ...editAccessCiphers],
+ this.organization.id,
+ );
+ }
+
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
this.refresh();
}
@@ -1030,7 +1068,10 @@ export class VaultComponent implements OnInit, OnDestroy {
if (
this.flexibleCollectionsV1Enabled &&
!c.edit &&
- !this.organization.allowAdminAccessToAllCollectionItems
+ !this.organization.canEditAllCiphers(
+ this.flexibleCollectionsV1Enabled,
+ this.restrictProviderAccessEnabled,
+ )
) {
this.showMissingPermissionsError();
return;
@@ -1053,7 +1094,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
- await this.deleteCipherWithServer(c.id, permanent);
+ await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned);
this.platformUtilsService.showToast(
"success",
null,
@@ -1065,7 +1106,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
- async deleteCollection(collection: CollectionView): Promise {
+ async deleteCollection(collection: CollectionAdminView): Promise {
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
this.showMissingPermissionsError();
return;
@@ -1111,6 +1152,18 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
+ // Allow bulk deleting of Unassigned Items
+ const unassignedCiphers: string[] = [];
+ const assignedCiphers: string[] = [];
+
+ ciphers.map((c) => {
+ if (c.isUnassigned) {
+ unassignedCiphers.push(c.id);
+ } else {
+ assignedCiphers.push(c.id);
+ }
+ });
+
if (ciphers.length === 0 && collections.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
return;
@@ -1121,8 +1174,11 @@ export class VaultComponent implements OnInit, OnDestroy {
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
const canDeleteCiphers =
ciphers == null ||
- this.organization.allowAdminAccessToAllCollectionItems ||
- ciphers.every((c) => c.edit);
+ ciphers.every((c) => c.edit) ||
+ this.organization.canEditAllCiphers(
+ this.flexibleCollectionsV1Enabled,
+ this.restrictProviderAccessEnabled,
+ );
if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
this.showMissingPermissionsError();
@@ -1132,9 +1188,10 @@ export class VaultComponent implements OnInit, OnDestroy {
const dialog = openBulkDeleteDialog(this.dialogService, {
data: {
permanent: this.filter.type === "trash",
- cipherIds: ciphers.map((c) => c.id),
+ cipherIds: assignedCiphers,
collections: collections,
organization,
+ unassignedCiphers,
},
});
@@ -1331,11 +1388,12 @@ export class VaultComponent implements OnInit, OnDestroy {
});
}
- protected deleteCipherWithServer(id: string, permanent: boolean) {
- const asAdmin = this.organization?.canEditAllCiphers(
- this.flexibleCollectionsV1Enabled,
- this.restrictProviderAccessEnabled,
- );
+ protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) {
+ const asAdmin =
+ this.organization?.canEditAllCiphers(
+ this.flexibleCollectionsV1Enabled,
+ this.restrictProviderAccessEnabled,
+ ) || isUnassigned;
return permanent
? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin);
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index 22e2c54a59..d559b18f06 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -132,11 +132,7 @@ export abstract class CipherService {
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise;
restoreWithServer: (id: string, asAdmin?: boolean) => Promise;
- restoreManyWithServer: (
- ids: string[],
- organizationId?: string,
- asAdmin?: boolean,
- ) => Promise;
+ restoreManyWithServer: (ids: string[], orgId?: string) => Promise;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise;
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise;
}
diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts
index 911a78f565..028b582db2 100644
--- a/libs/common/src/vault/models/view/cipher.view.ts
+++ b/libs/common/src/vault/models/view/cipher.view.ts
@@ -126,6 +126,12 @@ export class CipherView implements View, InitializerMetadata {
return this.item?.linkedFieldOptions;
}
+ get isUnassigned(): boolean {
+ return (
+ this.organizationId != null && (this.collectionIds == null || this.collectionIds.length === 0)
+ );
+ }
+
linkedFieldValue(id: LinkedIdType) {
const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) {
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index 537245459e..0e6ddf40ca 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -1117,14 +1117,15 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore({ id: id, revisionDate: response.revisionDate });
}
- async restoreManyWithServer(
- ids: string[],
- organizationId: string = null,
- asAdmin = false,
- ): Promise {
+ /**
+ * No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
+ * The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
+ */
+ async restoreManyWithServer(ids: string[], orgId: string = null): Promise {
let response;
- if (asAdmin) {
- const request = new CipherBulkRestoreRequest(ids, organizationId);
+
+ if (orgId) {
+ const request = new CipherBulkRestoreRequest(ids, orgId);
response = await this.apiService.putRestoreManyCiphersAdmin(request);
} else {
const request = new CipherBulkRestoreRequest(ids);