1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

[AC-1139] Flexible collections: deprecate Manage/Edit/Delete Assigned Collections custom permissions (#6906)

* [AC-1139] Add new layout for MemberDialogComponent when FC feature flag is enabled

* [AC-1139] Deprecated Organization canEditAssignedCollections, canDeleteAssignedCollections, canViewAssignedCollections

* [AC-1139] Checking if FC feature flag is enabled when using canDeleteAssignedCollections or canViewAssignedCollections

* [AC-1139] Added missing parameter to customRedirect

* [AC-1139] Fixed canEdit permission

* [AC-1139] Fixed CanDelete logic

* [AC-1139] Changed canAccessVaultTab function to receive configService

* Override deprecated values on sync

* [AC-1139] Reverted change that introduced ConfigService as a parameter to canAccessVaultTab

* [AC-1139] Fixed circular dependency

* [AC-1139] Moved overriding of deprecated values to syncService

* Revert "[AC-1139] Fixed circular dependency"

This reverts commit 6484420976.

* Revert "Override deprecated values on sync"

This reverts commit f0c25a6996.

* [AC-1139] Added back the deprecation of methods canEditAssignedCollections, canDeleteAssignedCollections, canViewAssignedCollections

* [AC-1139] Reverted change on syncService

* [AC-1139] Override deprecated values on sync

* [AC-1139] Fix canDelete logic in
collection-dialog.component.ts and
bulk-delete-dialog.component.ts

* [AC-1139] Moved override logic from syncService to organizationService

* [AC-1139] Add ability to have titlecase titles on nested-checkbox.component checkboxes; use on member-dialog.component

* Revert "[AC-1139] Add ability to have titlecase titles on nested-checkbox.component checkboxes; use on member-dialog.component"

This reverts commit 9ede0fc5ac.

* [AC-1139] Fix bulk delete functionality

* [AC-1139] Refactor canEdit and canDelete to use ternary operator

* [AC-1139] Fix canDelete condition in VaultComponent

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Rui Tomé
2023-12-08 18:07:52 +00:00
committed by GitHub
parent 7c285c5990
commit 483a197e4d
20 changed files with 305 additions and 118 deletions

View File

@@ -539,6 +539,7 @@ export default class MainBackground {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.configService,
logoutCallback,
);
this.eventUploadService = new EventUploadService(

View File

@@ -443,6 +443,7 @@ export class Main {
this.folderApiService,
this.organizationService,
this.sendApiService,
this.configService,
async (expired: boolean) => await this.logout(),
);

View File

@@ -138,25 +138,128 @@
</div>
</fieldset>
<ng-container *ngIf="customUserTypeSelected">
<h3 class="mt-4 d-flex tw-font-semibold">
{{ "permissions" | i18n }}
</h3>
<div class="row" [formGroup]="permissionsGroup">
<div class="col-6">
<div class="mb-3">
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
<app-nested-checkbox
parentId="manageAssignedCollections"
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
>
</app-nested-checkbox>
<ng-container *ngIf="!(flexibleCollectionsEnabled$ | async); else customPermissionsFC">
<h3 class="mt-4 d-flex tw-font-semibold">
{{ "permissions" | i18n }}
</h3>
<div class="row" [formGroup]="permissionsGroup">
<div class="col-6">
<div class="mb-3">
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
<app-nested-checkbox
parentId="manageAssignedCollections"
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
>
</app-nested-checkbox>
</div>
</div>
<div class="col-6">
<div class="mb-3">
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
<div>
<input
type="checkbox"
name="accessEventLogs"
id="accessEventLogs"
formControlName="accessEventLogs"
/>
<label class="!tw-font-normal" for="accessEventLogs">
{{ "accessEventLogs" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="accessImportExport"
id="accessImportExport"
formControlName="accessImportExport"
/>
<label class="!tw-font-normal" for="accessImportExport">
{{ "accessImportExport" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="accessReports"
id="accessReports"
formControlName="accessReports"
/>
<label class="!tw-font-normal" for="accessReports">
{{ "accessReports" | i18n }}
</label>
</div>
<app-nested-checkbox
parentId="manageAllCollections"
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
>
</app-nested-checkbox>
<div>
<input
type="checkbox"
name="manageGroups"
id="manageGroups"
formControlName="manageGroups"
/>
<label class="!tw-font-normal" for="manageGroups">
{{ "manageGroups" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageSso"
id="manageSso"
formControlName="manageSso"
/>
<label class="!tw-font-normal" for="manageSso">
{{ "manageSso" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="managePolicies"
id="managePolicies"
formControlName="managePolicies"
/>
<label class="!tw-font-normal" for="managePolicies">
{{ "managePolicies" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageUsers"
id="manageUsers"
formControlName="manageUsers"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageUsers">
{{ "manageUsers" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageResetPassword"
id="manageResetPassword"
formControlName="manageResetPassword"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageResetPassword">
{{ "manageAccountRecovery" | i18n }}
</label>
</div>
</div>
</div>
</div>
<div class="col-6">
<div class="mb-3">
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
</ng-container>
<ng-template #customPermissionsFC>
<div class="row" [formGroup]="permissionsGroup">
<div class="col-4">
<div>
<input
type="checkbox"
@@ -190,71 +293,77 @@
{{ "accessReports" | i18n }}
</label>
</div>
</div>
<div class="col-4">
<app-nested-checkbox
parentId="manageAllCollections"
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
>
</app-nested-checkbox>
<div>
<input
type="checkbox"
name="manageGroups"
id="manageGroups"
formControlName="manageGroups"
/>
<label class="!tw-font-normal" for="manageGroups">
{{ "manageGroups" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageSso"
id="manageSso"
formControlName="manageSso"
/>
<label class="!tw-font-normal" for="manageSso">
{{ "manageSso" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="managePolicies"
id="managePolicies"
formControlName="managePolicies"
/>
<label class="!tw-font-normal" for="managePolicies">
{{ "managePolicies" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageUsers"
id="manageUsers"
formControlName="manageUsers"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageUsers">
{{ "manageUsers" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageResetPassword"
id="manageResetPassword"
formControlName="manageResetPassword"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageResetPassword">
{{ "manageAccountRecovery" | i18n }}
</label>
</div>
<div class="col-4">
<div class="mb-3">
<div>
<input
type="checkbox"
name="manageGroups"
id="manageGroups"
formControlName="manageGroups"
/>
<label class="!tw-font-normal" for="manageGroups">
{{ "manageGroups" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageSso"
id="manageSso"
formControlName="manageSso"
/>
<label class="!tw-font-normal" for="manageSso">
{{ "manageSso" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="managePolicies"
id="managePolicies"
formControlName="managePolicies"
/>
<label class="!tw-font-normal" for="managePolicies">
{{ "managePolicies" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageUsers"
id="manageUsers"
formControlName="manageUsers"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageUsers">
{{ "manageUsers" | i18n }}
</label>
</div>
<div>
<input
type="checkbox"
name="manageResetPassword"
id="manageResetPassword"
formControlName="manageResetPassword"
(change)="handleDependentPermissions()"
/>
<label class="!tw-font-normal" for="manageResetPassword">
{{ "manageAccountRecovery" | i18n }}
</label>
</div>
</div>
</div>
</div>
</div>
</ng-template>
</ng-container>
<ng-container *ngIf="canUseSecretsManager">
<h3 class="mt-4">

View File

@@ -9,6 +9,8 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -33,6 +35,7 @@ export class SecretsManagerSubscribeStandaloneComponent {
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationService: InternalOrganizationServiceAbstraction,
private configService: ConfigServiceAbstraction,
) {}
submit = async () => {
@@ -52,7 +55,11 @@ export class SecretsManagerSubscribeStandaloneComponent {
isMember: this.organization.isMember,
isProviderUser: this.organization.isProviderUser,
});
await this.organizationService.upsert(organizationData);
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollections,
false,
);
await this.organizationService.upsert(organizationData, flexibleCollectionsEnabled);
/*
Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the

View File

@@ -109,7 +109,7 @@
{{ "cancel" | i18n }}
</button>
<button
*ngIf="editMode && organization?.canDeleteAssignedCollections"
*ngIf="canDelete$ | async"
type="button"
bitIconButton="bwi-trash"
buttonType="danger"

View File

@@ -313,6 +313,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.close(CollectionDialogAction.Deleted, this.collection);
};
protected canDelete$ = this.flexibleCollectionsEnabled$.pipe(
map(
(flexibleCollectionsEnabled) =>
this.editMode && this.collection.canDelete(this.organization, flexibleCollectionsEnabled),
),
);
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();

View File

@@ -106,7 +106,7 @@ export class VaultItemsComponent {
}
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canEdit(organization);
return collection.canEdit(organization, this.flexibleCollectionsEnabled);
}
protected canDeleteCollection(collection: CollectionView): boolean {

View File

@@ -31,15 +31,15 @@ export class CollectionAdminView extends CollectionView {
this.assigned = response.assigned;
}
override canEdit(org: Organization): boolean {
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
override canEdit(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
return flexibleCollectionsEnabled
? org?.canEditAnyCollection
: org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
}
override canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
if (flexibleCollectionsEnabled) {
return org?.canDeleteAnyCollection;
} else {
return org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
}
return flexibleCollectionsEnabled
? org?.canDeleteAnyCollection
: org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
}
}

View File

@@ -3,6 +3,8 @@ import { Component, Inject } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -59,6 +61,7 @@ export class BulkDeleteDialogComponent {
private i18nService: I18nService,
private apiService: ApiService,
private collectionService: CollectionService,
private configService: ConfigServiceAbstraction,
) {
this.cipherIds = params.cipherIds ?? [];
this.collectionIds = params.collectionIds ?? [];
@@ -125,11 +128,14 @@ export class BulkDeleteDialogComponent {
}
private async deleteCollections(): Promise<any> {
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollections,
false,
);
// From org vault
if (this.organization) {
if (
!this.organization.canDeleteAssignedCollections &&
!this.organization.canDeleteAnyCollection
this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsEnabled))
) {
this.platformUtilsService.showToast(
"error",
@@ -143,7 +149,8 @@ export class BulkDeleteDialogComponent {
} else if (this.organizations && this.collections) {
const deletePromises: Promise<any>[] = [];
for (const organization of this.organizations) {
if (!organization.canDeleteAssignedCollections && !organization.canDeleteAnyCollection) {
const orgCollections = this.collections.filter((o) => o.organizationId === organization.id);
if (orgCollections.some((c) => !c.canDelete(organization, flexibleCollectionsEnabled))) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
@@ -151,11 +158,9 @@ export class BulkDeleteDialogComponent {
);
return;
}
const orgCollections = this.collections
.filter((o) => o.organizationId === organization.id)
.map((c) => c.id);
const orgCollectionIds = orgCollections.map((c) => c.id);
deletePromises.push(
this.apiService.deleteManyCollections(this.organization.id, orgCollections),
this.apiService.deleteManyCollections(this.organization.id, orgCollectionIds),
);
}
return await Promise.all(deletePromises);

View File

@@ -146,7 +146,7 @@ export class VaultHeaderComponent {
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canEdit(organization);
return this.collection.node.canEdit(organization, this.flexibleCollectionsEnabled);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {

View File

@@ -688,7 +688,11 @@ export class VaultComponent implements OnInit, OnDestroy {
async deleteCollection(collection: CollectionView): Promise<void> {
const organization = this.organizationService.get(collection.organizationId);
if (!organization.canDeleteAssignedCollections && !organization.canDeleteAnyCollection) {
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollections,
false,
);
if (!collection.canDelete(organization, flexibleCollectionsEnabled)) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),

View File

@@ -152,7 +152,7 @@ export class VaultHeaderComponent {
}
// Otherwise, check if we can edit the specified collection
return this.collection.node.canEdit(this.organization);
return this.collection.node.canEdit(this.organization, this.flexibleCollectionsEnabled);
}
addCipher() {

View File

@@ -132,6 +132,7 @@ export class VaultComponent implements OnInit, OnDestroy {
FeatureFlag.BulkCollectionAccess,
false,
);
protected flexibleCollectionsEnabled: boolean;
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
@@ -750,10 +751,11 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async deleteCollection(collection: CollectionView): Promise<void> {
if (
!this.organization.canDeleteAssignedCollections &&
!this.organization.canDeleteAnyCollection
) {
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollections,
false,
);
if (!collection.canDelete(this.organization, flexibleCollectionsEnabled)) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),