1
0
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:
Thomas Rittson 2024-05-15 08:29:54 +10:00 committed by GitHub
parent 3eeafc098a
commit 6ab7336c21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 221 additions and 94 deletions

View File

@ -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"

View File

@ -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;
}
} }

View File

@ -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;
} }

View File

@ -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)
);
} }
/** /**

View File

@ -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$);
}
} }
/** /**

View File

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

View File

@ -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"),
});
}
} }
/** /**

View File

@ -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"
}, },

View File

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