1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[AC- 2493] Restore and Delete Unassigned Items (#8983)

* updates added for single and bulk delete and restore items including unassigned and permissions for owners and custom users
This commit is contained in:
Jason Ng
2024-05-21 12:32:02 -04:00
committed by GitHub
parent dff44b02e2
commit b7463d551c
6 changed files with 109 additions and 36 deletions

View File

@@ -4,8 +4,8 @@
</span> </span>
<span bitDialogContent> <span bitDialogContent>
<ng-container *ngIf="!permanent"> <ng-container *ngIf="!permanent">
<span *ngIf="cipherIds?.length"> <span *ngIf="cipherIds?.length || unassignedCiphers?.length">
{{ "deleteSelectedItemsDesc" | i18n: cipherIds.length }} {{ "deleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
</span> </span>
<span *ngIf="collections?.length"> <span *ngIf="collections?.length">
{{ "deleteSelectedCollectionsDesc" | i18n: collections.length }} {{ "deleteSelectedCollectionsDesc" | i18n: collections.length }}
@@ -13,7 +13,7 @@
{{ "deleteSelectedConfirmation" | i18n }} {{ "deleteSelectedConfirmation" | i18n }}
</ng-container> </ng-container>
<ng-container *ngIf="permanent"> <ng-container *ngIf="permanent">
{{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length }} {{ "permanentlyDeleteSelectedItemsDesc" | i18n: cipherIds.length + unassignedCiphers.length }}
</ng-container> </ng-container>
</span> </span>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>

View File

@@ -20,6 +20,7 @@ export interface BulkDeleteDialogParams {
organization?: Organization; organization?: Organization;
organizations?: Organization[]; organizations?: Organization[];
collections?: CollectionView[]; collections?: CollectionView[];
unassignedCiphers?: string[];
} }
export enum BulkDeleteDialogResult { export enum BulkDeleteDialogResult {
@@ -51,6 +52,7 @@ export class BulkDeleteDialogComponent {
organization: Organization; organization: Organization;
organizations: Organization[]; organizations: Organization[];
collections: CollectionView[]; collections: CollectionView[];
unassignedCiphers: string[];
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1, FeatureFlag.FlexibleCollectionsV1,
@@ -75,6 +77,7 @@ export class BulkDeleteDialogComponent {
this.organization = params.organization; this.organization = params.organization;
this.organizations = params.organizations; this.organizations = params.organizations;
this.collections = params.collections; this.collections = params.collections;
this.unassignedCiphers = params.unassignedCiphers || [];
} }
protected async cancel() { protected async cancel() {
@@ -83,6 +86,15 @@ export class BulkDeleteDialogComponent {
protected submit = async () => { protected submit = async () => {
const deletePromises: Promise<void>[] = []; const deletePromises: Promise<void>[] = [];
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) { if (this.cipherIds.length) {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
@@ -93,7 +105,7 @@ export class BulkDeleteDialogComponent {
) { ) {
deletePromises.push(this.deleteCiphers()); deletePromises.push(this.deleteCiphers());
} else { } else {
deletePromises.push(this.deleteCiphersAdmin()); deletePromises.push(this.deleteCiphersAdmin(this.cipherIds));
} }
} }
@@ -103,7 +115,7 @@ export class BulkDeleteDialogComponent {
await Promise.all(deletePromises); await Promise.all(deletePromises);
if (this.cipherIds.length) { if (this.cipherIds.length || this.unassignedCiphers.length) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
null, null,
@@ -135,8 +147,8 @@ export class BulkDeleteDialogComponent {
} }
} }
private async deleteCiphersAdmin(): Promise<any> { private async deleteCiphersAdmin(ciphers: string[]): Promise<any> {
const deleteRequest = new CipherBulkDeleteRequest(this.cipherIds, this.organization.id); const deleteRequest = new CipherBulkDeleteRequest(ciphers, this.organization.id);
if (this.permanent) { if (this.permanent) {
return await this.apiService.deleteManyCiphersAdmin(deleteRequest); return await this.apiService.deleteManyCiphersAdmin(deleteRequest);
} else { } else {

View File

@@ -751,7 +751,7 @@ export class VaultComponent implements OnInit, OnDestroy {
if (ciphers.length === 1 && collections.length === 0) { if (ciphers.length === 1 && collections.length === 0) {
await this.deleteCipher(ciphers[0]); await this.deleteCipher(ciphers[0]);
} else if (ciphers.length === 0 && collections.length === 1) { } else if (ciphers.length === 0 && collections.length === 1) {
await this.deleteCollection(collections[0]); await this.deleteCollection(collections[0] as CollectionAdminView);
} else { } else {
await this.bulkDelete(ciphers, collections, this.organization); await this.bulkDelete(ciphers, collections, this.organization);
} }
@@ -980,6 +980,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
if ( if (
!this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled && this.flexibleCollectionsV1Enabled &&
!c.edit && !c.edit &&
!this.organization.allowAdminAccessToAllCollectionItems !this.organization.allowAdminAccessToAllCollectionItems
@@ -992,8 +993,11 @@ export class VaultComponent implements OnInit, OnDestroy {
return; return;
} }
// Allow restore of an Unassigned Item
try { try {
const asAdmin = this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled); const asAdmin =
this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
c.isUnassigned;
await this.cipherService.restoreWithServer(c.id, asAdmin); await this.cipherService.restoreWithServer(c.id, asAdmin);
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
this.refresh(); this.refresh();
@@ -1004,6 +1008,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async bulkRestore(ciphers: CipherView[]) { async bulkRestore(ciphers: CipherView[]) {
if ( if (
!this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled && this.flexibleCollectionsV1Enabled &&
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems) ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
) { ) {
@@ -1015,13 +1020,46 @@ export class VaultComponent implements OnInit, OnDestroy {
return; return;
} }
const selectedCipherIds = ciphers.map((cipher) => cipher.id); // assess if there are unassigned ciphers and/or editable ciphers selected in bulk for restore
if (selectedCipherIds.length === 0) { const editAccessCiphers: string[] = [];
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected")); 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; 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.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItems"));
this.refresh(); this.refresh();
} }
@@ -1030,7 +1068,10 @@ export class VaultComponent implements OnInit, OnDestroy {
if ( if (
this.flexibleCollectionsV1Enabled && this.flexibleCollectionsV1Enabled &&
!c.edit && !c.edit &&
!this.organization.allowAdminAccessToAllCollectionItems !this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
)
) { ) {
this.showMissingPermissionsError(); this.showMissingPermissionsError();
return; return;
@@ -1053,7 +1094,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
try { try {
await this.deleteCipherWithServer(c.id, permanent); await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",
null, null,
@@ -1065,7 +1106,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
} }
async deleteCollection(collection: CollectionView): Promise<void> { async deleteCollection(collection: CollectionAdminView): Promise<void> {
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) { if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
this.showMissingPermissionsError(); this.showMissingPermissionsError();
return; return;
@@ -1111,6 +1152,18 @@ export class VaultComponent implements OnInit, OnDestroy {
return; 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) { if (ciphers.length === 0 && collections.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected")); this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
return; return;
@@ -1121,8 +1174,11 @@ export class VaultComponent implements OnInit, OnDestroy {
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled)); collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
const canDeleteCiphers = const canDeleteCiphers =
ciphers == null || 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)) { if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
this.showMissingPermissionsError(); this.showMissingPermissionsError();
@@ -1132,9 +1188,10 @@ export class VaultComponent implements OnInit, OnDestroy {
const dialog = openBulkDeleteDialog(this.dialogService, { const dialog = openBulkDeleteDialog(this.dialogService, {
data: { data: {
permanent: this.filter.type === "trash", permanent: this.filter.type === "trash",
cipherIds: ciphers.map((c) => c.id), cipherIds: assignedCiphers,
collections: collections, collections: collections,
organization, organization,
unassignedCiphers,
}, },
}); });
@@ -1331,11 +1388,12 @@ export class VaultComponent implements OnInit, OnDestroy {
}); });
} }
protected deleteCipherWithServer(id: string, permanent: boolean) { protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) {
const asAdmin = this.organization?.canEditAllCiphers( const asAdmin =
this.flexibleCollectionsV1Enabled, this.organization?.canEditAllCiphers(
this.restrictProviderAccessEnabled, this.flexibleCollectionsV1Enabled,
); this.restrictProviderAccessEnabled,
) || isUnassigned;
return permanent return permanent
? this.cipherService.deleteWithServer(id, asAdmin) ? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin); : this.cipherService.softDeleteWithServer(id, asAdmin);

View File

@@ -132,11 +132,7 @@ export abstract class CipherService {
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise<any>; ) => Promise<any>;
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>; restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>;
restoreManyWithServer: ( restoreManyWithServer: (ids: string[], orgId?: string) => Promise<void>;
ids: string[],
organizationId?: string,
asAdmin?: boolean,
) => Promise<void>;
getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>;
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>; setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>;
} }

View File

@@ -126,6 +126,12 @@ export class CipherView implements View, InitializerMetadata {
return this.item?.linkedFieldOptions; return this.item?.linkedFieldOptions;
} }
get isUnassigned(): boolean {
return (
this.organizationId != null && (this.collectionIds == null || this.collectionIds.length === 0)
);
}
linkedFieldValue(id: LinkedIdType) { linkedFieldValue(id: LinkedIdType) {
const linkedFieldOption = this.linkedFieldOptions?.get(id); const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) { if (linkedFieldOption == null) {

View File

@@ -1117,14 +1117,15 @@ export class CipherService implements CipherServiceAbstraction {
await this.restore({ id: id, revisionDate: response.revisionDate }); await this.restore({ id: id, revisionDate: response.revisionDate });
} }
async restoreManyWithServer( /**
ids: string[], * No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
organizationId: string = null, * The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
asAdmin = false, */
): Promise<void> { async restoreManyWithServer(ids: string[], orgId: string = null): Promise<void> {
let response; let response;
if (asAdmin) {
const request = new CipherBulkRestoreRequest(ids, organizationId); if (orgId) {
const request = new CipherBulkRestoreRequest(ids, orgId);
response = await this.apiService.putRestoreManyCiphersAdmin(request); response = await this.apiService.putRestoreManyCiphersAdmin(request);
} else { } else {
const request = new CipherBulkRestoreRequest(ids); const request = new CipherBulkRestoreRequest(ids);