mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-23 21:31:29 +01:00
[AC-2499] Add permission checks on bulk actions menu (#8912)
* Add permission checks for org vault bulk actions * Show checkboxes for all collections except Unassigned * Separate individual and admin logic between CollectionView and CollectionAdminView * Remove heading for error toasts per design feedback
This commit is contained in:
parent
3eeafc098a
commit
6ab7336c21
@ -3,7 +3,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
appStopProp
|
appStopProp
|
||||||
*ngIf="canDeleteCollection"
|
*ngIf="showCheckbox"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
[checked]="checked"
|
[checked]="checked"
|
||||||
(change)="$event ? this.checkedToggled.next() : null"
|
(change)="$event ? this.checkedToggled.next() : null"
|
||||||
|
@ -90,4 +90,12 @@ export class VaultCollectionRowComponent {
|
|||||||
protected deleteCollection() {
|
protected deleteCollection() {
|
||||||
this.onEvent.next({ type: "delete", items: [{ collection: this.collection }] });
|
this.onEvent.next({ type: "delete", items: [{ collection: this.collection }] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get showCheckbox() {
|
||||||
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
|
return this.collection?.id !== Unassigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.canDeleteCollection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,11 +245,23 @@ export class VaultItemsComponent {
|
|||||||
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
const items: VaultItem[] = [].concat(collections).concat(ciphers);
|
||||||
|
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this.editableItems = items.filter(
|
|
||||||
(item) =>
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
item.cipher !== undefined ||
|
// Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission
|
||||||
(item.collection !== undefined && this.canDeleteCollection(item.collection)),
|
this.editableItems = items.filter(
|
||||||
);
|
(item) =>
|
||||||
|
item.cipher !== undefined ||
|
||||||
|
(item.collection !== undefined && item.collection.id !== Unassigned),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// only collections the user can delete are selectable
|
||||||
|
this.editableItems = items.filter(
|
||||||
|
(item) =>
|
||||||
|
item.cipher !== undefined ||
|
||||||
|
(item.collection !== undefined && this.canDeleteCollection(item.collection)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.dataSource.data = items;
|
this.dataSource.data = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,13 +62,23 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the current user can edit the collection, including user and group access
|
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
|
||||||
*/
|
*/
|
||||||
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
return org?.flexibleCollections
|
return (
|
||||||
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
|
org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
|
||||||
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
|
super.canEdit(org, flexibleCollectionsV1Enabled)
|
||||||
(org?.canEditAssignedCollections && this.assigned);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user can delete a collection from the Admin Console.
|
||||||
|
*/
|
||||||
|
override canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
|
return (
|
||||||
|
org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) ||
|
||||||
|
super.canDelete(org, flexibleCollectionsV1Enabled)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,7 +47,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -61,7 +60,7 @@ import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/respon
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import { DialogService, Icons } from "@bitwarden/components";
|
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -167,7 +166,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private stateService: StateService,
|
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private vaultFilterService: VaultFilterService,
|
private vaultFilterService: VaultFilterService,
|
||||||
private routedVaultFilterService: RoutedVaultFilterService,
|
private routedVaultFilterService: RoutedVaultFilterService,
|
||||||
@ -184,6 +182,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
private toastService: ToastService,
|
||||||
protected kdfConfigService: KdfConfigService,
|
protected kdfConfigService: KdfConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -347,7 +346,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
null,
|
||||||
this.i18nService.t("unknownCipher"),
|
this.i18nService.t("unknownCipher"),
|
||||||
);
|
);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
@ -551,6 +550,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async shareCipher(cipher: CipherView) {
|
async shareCipher(cipher: CipherView) {
|
||||||
|
if ((await this.flexibleCollectionsV1Enabled()) && cipher.organizationId != null) {
|
||||||
|
// You cannot move ciphers between organizations
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
if (cipher?.reprompt !== 0 && !(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||||
this.go({ cipherId: null, itemId: null });
|
this.go({ cipherId: null, itemId: null });
|
||||||
return;
|
return;
|
||||||
@ -700,11 +705,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
const organization = await this.organizationService.get(collection.organizationId);
|
const organization = await this.organizationService.get(collection.organizationId);
|
||||||
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||||
if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) {
|
if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) {
|
||||||
this.platformUtilsService.showToast(
|
this.showMissingPermissionsError();
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("missingPermissions"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
@ -755,11 +756,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restore(c: CipherView): Promise<boolean> {
|
async restore(c: CipherView): Promise<boolean> {
|
||||||
if (!(await this.repromptCipher([c]))) {
|
if (!c.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!c.isDeleted) {
|
if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.repromptCipher([c]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,17 +779,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bulkRestore(ciphers: CipherView[]) {
|
async bulkRestore(ciphers: CipherView[]) {
|
||||||
|
if ((await this.flexibleCollectionsV1Enabled()) && ciphers.some((c) => !c.edit)) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await this.repromptCipher(ciphers))) {
|
if (!(await this.repromptCipher(ciphers))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
||||||
if (selectedCipherIds.length === 0) {
|
if (selectedCipherIds.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -817,6 +824,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const permanent = c.isDeleted;
|
const permanent = c.isDeleted;
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
@ -852,13 +864,27 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ciphers.length === 0 && collections.length === 0) {
|
if (ciphers.length === 0 && collections.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled();
|
||||||
|
|
||||||
|
const canDeleteCollections =
|
||||||
|
collections == null ||
|
||||||
|
collections.every((c) =>
|
||||||
|
c.canDelete(
|
||||||
|
organizations.find((o) => o.id == c.organizationId),
|
||||||
|
flexibleCollectionsV1Enabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const canDeleteCiphers = ciphers == null || ciphers.every((c) => c.edit);
|
||||||
|
|
||||||
|
if (flexibleCollectionsV1Enabled && (!canDeleteCollections || !canDeleteCiphers)) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
const dialog = openBulkDeleteDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
permanent: this.filter.type === "trash",
|
permanent: this.filter.type === "trash",
|
||||||
@ -881,11 +907,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
||||||
if (selectedCipherIds.length === 0) {
|
if (selectedCipherIds.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -955,12 +977,17 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(await this.flexibleCollectionsV1Enabled()) &&
|
||||||
|
ciphers.some((c) => c.organizationId != null)
|
||||||
|
) {
|
||||||
|
// You cannot move ciphers between organizations
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (ciphers.length === 0) {
|
if (ciphers.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1016,6 +1043,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showMissingPermissionsError() {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("missingPermissions"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private flexibleCollectionsV1Enabled() {
|
||||||
|
return firstValueFrom(this.flexibleCollectionsV1Enabled$);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,6 +70,13 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// If no ciphers are passed in, close the dialog
|
||||||
|
if (this.params.ciphers == null || this.params.ciphers.length < 1) {
|
||||||
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
|
this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
|
const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1);
|
||||||
const restrictProviderAccess = await this.configService.getFeatureFlag(
|
const restrictProviderAccess = await this.configService.getFeatureFlag(
|
||||||
FeatureFlag.RestrictProviderAccess,
|
FeatureFlag.RestrictProviderAccess,
|
||||||
@ -86,12 +93,9 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni
|
|||||||
|
|
||||||
// If no ciphers are editable, close the dialog
|
// If no ciphers are editable, close the dialog
|
||||||
if (this.editableItemCount == 0) {
|
if (this.editableItemCount == 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("missingPermissions"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled);
|
this.dialogRef.close(BulkCollectionAssignmentDialogResult.Canceled);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.totalItemCount = this.params.ciphers.length;
|
this.totalItemCount = this.params.ciphers.length;
|
||||||
|
@ -59,7 +59,7 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
|||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||||
import { DialogService, Icons } from "@bitwarden/components";
|
import { DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { GroupService, GroupView } from "../../admin-console/organizations/core";
|
import { GroupService, GroupView } from "../../admin-console/organizations/core";
|
||||||
@ -152,7 +152,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
* A list of collections that the user can assign items to and edit those items within.
|
* A list of collections that the user can assign items to and edit those items within.
|
||||||
* @protected
|
* @protected
|
||||||
*/
|
*/
|
||||||
protected editableCollections$: Observable<CollectionView[]>;
|
protected editableCollections$: Observable<CollectionAdminView[]>;
|
||||||
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
|
||||||
private _flexibleCollectionsV1FlagEnabled: boolean;
|
private _flexibleCollectionsV1FlagEnabled: boolean;
|
||||||
|
|
||||||
@ -200,6 +200,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
protected configService: ConfigService,
|
protected configService: ConfigService,
|
||||||
|
private toastService: ToastService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -567,11 +568,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
if (canEditCipher) {
|
if (canEditCipher) {
|
||||||
await this.editCipherId(cipherId);
|
await this.editCipherId(cipherId);
|
||||||
} else {
|
} else {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unknownCipher"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("unknownCipher"),
|
|
||||||
);
|
|
||||||
await this.router.navigate([], {
|
await this.router.navigate([], {
|
||||||
queryParams: { cipherId: null, itemId: null },
|
queryParams: { cipherId: null, itemId: null },
|
||||||
queryParamsHandling: "merge",
|
queryParamsHandling: "merge",
|
||||||
@ -596,11 +593,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.viewEvents(cipher);
|
this.viewEvents(cipher);
|
||||||
} else {
|
} else {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unknownCipher"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("unknownCipher"),
|
|
||||||
);
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
@ -765,7 +758,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
} else if (event.type === "viewCollectionAccess") {
|
} else if (event.type === "viewCollectionAccess") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Access, event.readonly);
|
await this.editCollection(event.item, CollectionDialogTabType.Access, event.readonly);
|
||||||
} else if (event.type === "bulkEditCollectionAccess") {
|
} else if (event.type === "bulkEditCollectionAccess") {
|
||||||
await this.bulkEditCollectionAccess(event.items);
|
await this.bulkEditCollectionAccess(event.items, this.organization);
|
||||||
} else if (event.type === "assignToCollections") {
|
} else if (event.type === "assignToCollections") {
|
||||||
await this.bulkAssignToCollections(event.items);
|
await this.bulkAssignToCollections(event.items);
|
||||||
} else if (event.type === "viewEvents") {
|
} else if (event.type === "viewEvents") {
|
||||||
@ -817,7 +810,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async editCipherCollections(cipher: CipherView) {
|
async editCipherCollections(cipher: CipherView) {
|
||||||
let collections: CollectionView[] = [];
|
let collections: CollectionAdminView[] = [];
|
||||||
|
|
||||||
if (this.flexibleCollectionsV1Enabled) {
|
if (this.flexibleCollectionsV1Enabled) {
|
||||||
// V1 limits admins to only adding items to collections they have access to.
|
// V1 limits admins to only adding items to collections they have access to.
|
||||||
@ -978,11 +971,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restore(c: CipherView): Promise<boolean> {
|
async restore(c: CipherView): Promise<boolean> {
|
||||||
if (!(await this.repromptCipher([c]))) {
|
if (!c.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!c.isDeleted) {
|
if (
|
||||||
|
this.flexibleCollectionsV1Enabled &&
|
||||||
|
!c.edit &&
|
||||||
|
!this.organization.allowAdminAccessToAllCollectionItems
|
||||||
|
) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.repromptCipher([c]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -997,17 +999,21 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bulkRestore(ciphers: CipherView[]) {
|
async bulkRestore(ciphers: CipherView[]) {
|
||||||
|
if (
|
||||||
|
this.flexibleCollectionsV1Enabled &&
|
||||||
|
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
|
||||||
|
) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await this.repromptCipher(ciphers))) {
|
if (!(await this.repromptCipher(ciphers))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
const selectedCipherIds = ciphers.map((cipher) => cipher.id);
|
||||||
if (selectedCipherIds.length === 0) {
|
if (selectedCipherIds.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,6 +1023,15 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteCipher(c: CipherView): Promise<boolean> {
|
async deleteCipher(c: CipherView): Promise<boolean> {
|
||||||
|
if (
|
||||||
|
this.flexibleCollectionsV1Enabled &&
|
||||||
|
!c.edit &&
|
||||||
|
!this.organization.allowAdminAccessToAllCollectionItems
|
||||||
|
) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(await this.repromptCipher([c]))) {
|
if (!(await this.repromptCipher([c]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1048,11 +1063,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||||
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
|
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
|
||||||
this.platformUtilsService.showToast(
|
this.showMissingPermissionsError();
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("missingPermissions"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
@ -1097,13 +1108,23 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ciphers.length === 0 && collections.length === 0) {
|
if (ciphers.length === 0 && collections.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canDeleteCollections =
|
||||||
|
collections == null ||
|
||||||
|
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
|
||||||
|
const canDeleteCiphers =
|
||||||
|
ciphers == null ||
|
||||||
|
this.organization.allowAdminAccessToAllCollectionItems ||
|
||||||
|
ciphers.every((c) => c.edit);
|
||||||
|
|
||||||
|
if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = openBulkDeleteDialog(this.dialogService, {
|
const dialog = openBulkDeleteDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
permanent: this.filter.type === "trash",
|
permanent: this.filter.type === "trash",
|
||||||
@ -1228,13 +1249,24 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkEditCollectionAccess(collections: CollectionView[]): Promise<void> {
|
async bulkEditCollectionAccess(
|
||||||
|
collections: CollectionView[],
|
||||||
|
organization: Organization,
|
||||||
|
): Promise<void> {
|
||||||
if (collections.length === 0) {
|
if (collections.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.toastService.showToast({
|
||||||
"error",
|
variant: "error",
|
||||||
this.i18nService.t("errorOccurred"),
|
title: null,
|
||||||
this.i18nService.t("nothingSelected"),
|
message: this.i18nService.t("noCollectionsSelected"),
|
||||||
);
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.flexibleCollectionsV1Enabled &&
|
||||||
|
collections.some((c) => !c.canEdit(organization, this.flexibleCollectionsV1Enabled))
|
||||||
|
) {
|
||||||
|
this.showMissingPermissionsError();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1253,11 +1285,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async bulkAssignToCollections(items: CipherView[]) {
|
async bulkAssignToCollections(items: CipherView[]) {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("nothingSelected"));
|
||||||
"error",
|
|
||||||
this.i18nService.t("errorOccurred"),
|
|
||||||
this.i18nService.t("nothingSelected"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1338,6 +1366,14 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
||||||
|
|
||||||
|
private showMissingPermissionsError() {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "error",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("missingPermissions"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8187,6 +8187,9 @@
|
|||||||
"viewAccess": {
|
"viewAccess": {
|
||||||
"message": "View access"
|
"message": "View access"
|
||||||
},
|
},
|
||||||
|
"noCollectionsSelected": {
|
||||||
|
"message": "You have not selected any collections."
|
||||||
|
},
|
||||||
"updateName": {
|
"updateName": {
|
||||||
"message": "Update name"
|
"message": "Update name"
|
||||||
},
|
},
|
||||||
|
@ -61,7 +61,10 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
return org?.canEditAnyCollection(false) || (org?.canEditAssignedCollections && this.assigned);
|
return org?.canEditAnyCollection(false) || (org?.canEditAssignedCollections && this.assigned);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For editing collection details, not the items within it.
|
/**
|
||||||
|
* Returns true if the user can edit a collection (including user and group access) from the individual vault.
|
||||||
|
* After FCv1, does not include admin permissions - see {@link CollectionAdminView.canEdit}.
|
||||||
|
*/
|
||||||
canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
if (org != null && org.id !== this.organizationId) {
|
if (org != null && org.id !== this.organizationId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -69,12 +72,18 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return org?.flexibleCollections
|
if (flexibleCollectionsV1Enabled) {
|
||||||
? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage
|
// Only use individual permissions, not admin permissions
|
||||||
: org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || org?.canEditAssignedCollections;
|
return this.manage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For deleting a collection, not the items within it.
|
/**
|
||||||
|
* Returns true if the user can delete a collection from the individual vault.
|
||||||
|
* After FCv1, does not include admin permissions - see {@link CollectionAdminView.canDelete}.
|
||||||
|
*/
|
||||||
canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
|
||||||
if (org != null && org.id !== this.organizationId) {
|
if (org != null && org.id !== this.organizationId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -83,6 +92,12 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canDeleteManagedCollections = !org?.limitCollectionCreationDeletion || org.isAdmin;
|
const canDeleteManagedCollections = !org?.limitCollectionCreationDeletion || org.isAdmin;
|
||||||
|
|
||||||
|
if (flexibleCollectionsV1Enabled) {
|
||||||
|
// Only use individual permissions, not admin permissions
|
||||||
|
return canDeleteManagedCollections && this.manage;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) ||
|
org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) ||
|
||||||
(canDeleteManagedCollections && this.manage)
|
(canDeleteManagedCollections && this.manage)
|
||||||
|
Loading…
Reference in New Issue
Block a user