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