mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +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:
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.organization?.canEditAllCiphers(
|
||||||
this.flexibleCollectionsV1Enabled,
|
this.flexibleCollectionsV1Enabled,
|
||||||
this.restrictProviderAccessEnabled,
|
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);
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user