diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 4e95bb4bcc..a0d7617ce8 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -214,7 +214,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { access: accessSelections, }); this.collection.manage = collection?.manage ?? false; // Get manage flag from sync data collection - this.showDeleteButton = !this.dialogReadonly && this.collection.canDelete(organization); + this.showDeleteButton = + !this.dialogReadonly && + this.collection.canDelete(organization, flexibleCollectionsV1); } else { this.nestOptions = collections; const parent = collections.find((c) => c.id === this.params.parentCollectionId); diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index f172a73b06..8fcf5b01b1 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -163,7 +163,7 @@ export class VaultItemsComponent { } } - return collection.canDelete(organization); + return collection.canDelete(organization, this.flexibleCollectionsV1Enabled); } protected canViewCollectionInfo(collection: CollectionView) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 41aa766e3a..c7cc85a37b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -4,6 +4,7 @@ import { applicationConfig, Meta, moduleMetadata, Story } from "@storybook/angul import { BehaviorSubject, of } from "rxjs"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -318,5 +319,6 @@ function createOrganization(i: number): Organization { organization.id = `organization-${i}`; organization.name = `Organization ${i}`; organization.type = OrganizationUserType.Owner; + organization.permissions = new PermissionsApi(); return organization; } diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index b8725688ce..ad23584f6c 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -71,12 +71,6 @@ export class CollectionAdminView extends CollectionView { (org?.canEditAssignedCollections && this.assigned); } - override canDelete(org: Organization): boolean { - return org?.flexibleCollections - ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) - : org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned); - } - /** * Whether the user can modify user access to this collection */ diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index f49c54ac32..ee036f5e3b 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -145,9 +145,12 @@ export class BulkDeleteDialogComponent { } private async deleteCollections(): Promise { + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); // From org vault if (this.organization) { - if (this.collections.some((c) => !c.canDelete(this.organization))) { + if ( + this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsV1Enabled)) + ) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), @@ -164,7 +167,7 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; for (const organization of this.organizations) { const orgCollections = this.collections.filter((o) => o.organizationId === organization.id); - if (orgCollections.some((c) => !c.canDelete(organization))) { + if (orgCollections.some((c) => !c.canDelete(organization, flexibleCollectionsV1Enabled))) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 08afd09982..9e69286277 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -176,7 +176,7 @@ export class VaultHeaderComponent implements OnInit { (o) => o.id === this.collection?.node.organizationId, ); - return this.collection.node.canDelete(organization); + return this.collection.node.canDelete(organization, this.flexibleCollectionsV1Enabled); } deleteCollection() { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index ca3525effa..49565bdcee 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -693,7 +693,8 @@ export class VaultComponent implements OnInit, OnDestroy { async deleteCollection(collection: CollectionView): Promise { const organization = await this.organizationService.get(collection.organizationId); - if (!collection.canDelete(organization)) { + const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 9cac154d20..58af5516cc 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -207,7 +207,7 @@ export class VaultHeaderComponent implements OnInit { } // Otherwise, check if we can delete the specified collection - return this.collection.node.canDelete(this.organization); + return this.collection.node.canDelete(this.organization, this.flexibleCollectionsV1Enabled); } get canViewCollectionInfo(): boolean { diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index f35c3b41bb..64823c9754 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -1047,7 +1047,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async deleteCollection(collection: CollectionView): Promise { - if (!collection.canDelete(this.organization)) { + if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) { this.platformUtilsService.showToast( "error", this.i18nService.t("errorOccurred"), diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 04840477df..752b792844 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -232,8 +232,23 @@ export class Organization { ); } - get canDeleteAnyCollection() { - return this.isAdmin || this.permissions.deleteAnyCollection; + /** + * @param flexibleCollectionsV1Enabled - Whether or not the V1 Flexible Collection feature flag is enabled + * @returns True if the user can delete any collection + */ + canDeleteAnyCollection(flexibleCollectionsV1Enabled: boolean) { + // Providers and Users with DeleteAnyCollection permission can always delete collections + if (this.isProviderUser || this.permissions.deleteAnyCollection) { + return true; + } + + // If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting + // Using explicit type checks because provider users are handled above and this mimics the server's permission checks closely + if (!flexibleCollectionsV1Enabled || this.allowAdminAccessToAllCollectionItems) { + return this.type == OrganizationUserType.Owner || this.type == OrganizationUserType.Admin; + } + + return false; } /** @@ -242,7 +257,9 @@ export class Organization { */ get canViewAllCollections() { // Admins can always see all collections even if collection management settings prevent them from editing them or seeing items - return this.isAdmin || this.permissions.editAnyCollection || this.canDeleteAnyCollection; + return ( + this.isAdmin || this.permissions.editAnyCollection || this.permissions.deleteAnyCollection + ); } /** diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 991f9abe55..5e49d0a6c0 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -75,16 +75,18 @@ export class CollectionView implements View, ITreeNodeObject { } // For deleting a collection, not the items within it. - canDelete(org: Organization): boolean { + canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", ); } - return org?.flexibleCollections - ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) - : org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections; + const canDeleteManagedCollections = !org?.limitCollectionCreationDeletion || org.isAdmin; + return ( + org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) || + (canDeleteManagedCollections && this.manage) + ); } /**