[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 commit6484420976
. * Revert "Override deprecated values on sync" This reverts commitf0c25a6996
. * [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 commit9ede0fc5ac
. * [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:
parent
7c285c5990
commit
483a197e4d
|
@ -539,6 +539,7 @@ export default class MainBackground {
|
||||||
this.folderApiService,
|
this.folderApiService,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.sendApiService,
|
this.sendApiService,
|
||||||
|
this.configService,
|
||||||
logoutCallback,
|
logoutCallback,
|
||||||
);
|
);
|
||||||
this.eventUploadService = new EventUploadService(
|
this.eventUploadService = new EventUploadService(
|
||||||
|
|
|
@ -443,6 +443,7 @@ export class Main {
|
||||||
this.folderApiService,
|
this.folderApiService,
|
||||||
this.organizationService,
|
this.organizationService,
|
||||||
this.sendApiService,
|
this.sendApiService,
|
||||||
|
this.configService,
|
||||||
async (expired: boolean) => await this.logout(),
|
async (expired: boolean) => await this.logout(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -138,25 +138,128 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<ng-container *ngIf="customUserTypeSelected">
|
<ng-container *ngIf="customUserTypeSelected">
|
||||||
<h3 class="mt-4 d-flex tw-font-semibold">
|
<ng-container *ngIf="!(flexibleCollectionsEnabled$ | async); else customPermissionsFC">
|
||||||
{{ "permissions" | i18n }}
|
<h3 class="mt-4 d-flex tw-font-semibold">
|
||||||
</h3>
|
{{ "permissions" | i18n }}
|
||||||
<div class="row" [formGroup]="permissionsGroup">
|
</h3>
|
||||||
<div class="col-6">
|
<div class="row" [formGroup]="permissionsGroup">
|
||||||
<div class="mb-3">
|
<div class="col-6">
|
||||||
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
|
<div class="mb-3">
|
||||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
<label class="tw-font-semibold">{{ "managerPermissions" | i18n }}</label>
|
||||||
<app-nested-checkbox
|
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
||||||
parentId="manageAssignedCollections"
|
<app-nested-checkbox
|
||||||
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
|
parentId="manageAssignedCollections"
|
||||||
>
|
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
|
||||||
</app-nested-checkbox>
|
>
|
||||||
|
</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>
|
</div>
|
||||||
<div class="col-6">
|
</ng-container>
|
||||||
<div class="mb-3">
|
<ng-template #customPermissionsFC>
|
||||||
<label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</label>
|
<div class="row" [formGroup]="permissionsGroup">
|
||||||
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
|
<div class="col-4">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -190,71 +293,77 @@
|
||||||
{{ "accessReports" | i18n }}
|
{{ "accessReports" | i18n }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
<app-nested-checkbox
|
<app-nested-checkbox
|
||||||
parentId="manageAllCollections"
|
parentId="manageAllCollections"
|
||||||
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
|
||||||
>
|
>
|
||||||
</app-nested-checkbox>
|
</app-nested-checkbox>
|
||||||
<div>
|
</div>
|
||||||
<input
|
<div class="col-4">
|
||||||
type="checkbox"
|
<div class="mb-3">
|
||||||
name="manageGroups"
|
<div>
|
||||||
id="manageGroups"
|
<input
|
||||||
formControlName="manageGroups"
|
type="checkbox"
|
||||||
/>
|
name="manageGroups"
|
||||||
<label class="!tw-font-normal" for="manageGroups">
|
id="manageGroups"
|
||||||
{{ "manageGroups" | i18n }}
|
formControlName="manageGroups"
|
||||||
</label>
|
/>
|
||||||
</div>
|
<label class="!tw-font-normal" for="manageGroups">
|
||||||
<div>
|
{{ "manageGroups" | i18n }}
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
name="manageSso"
|
<div>
|
||||||
id="manageSso"
|
<input
|
||||||
formControlName="manageSso"
|
type="checkbox"
|
||||||
/>
|
name="manageSso"
|
||||||
<label class="!tw-font-normal" for="manageSso">
|
id="manageSso"
|
||||||
{{ "manageSso" | i18n }}
|
formControlName="manageSso"
|
||||||
</label>
|
/>
|
||||||
</div>
|
<label class="!tw-font-normal" for="manageSso">
|
||||||
<div>
|
{{ "manageSso" | i18n }}
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
name="managePolicies"
|
<div>
|
||||||
id="managePolicies"
|
<input
|
||||||
formControlName="managePolicies"
|
type="checkbox"
|
||||||
/>
|
name="managePolicies"
|
||||||
<label class="!tw-font-normal" for="managePolicies">
|
id="managePolicies"
|
||||||
{{ "managePolicies" | i18n }}
|
formControlName="managePolicies"
|
||||||
</label>
|
/>
|
||||||
</div>
|
<label class="!tw-font-normal" for="managePolicies">
|
||||||
<div>
|
{{ "managePolicies" | i18n }}
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
name="manageUsers"
|
<div>
|
||||||
id="manageUsers"
|
<input
|
||||||
formControlName="manageUsers"
|
type="checkbox"
|
||||||
(change)="handleDependentPermissions()"
|
name="manageUsers"
|
||||||
/>
|
id="manageUsers"
|
||||||
<label class="!tw-font-normal" for="manageUsers">
|
formControlName="manageUsers"
|
||||||
{{ "manageUsers" | i18n }}
|
(change)="handleDependentPermissions()"
|
||||||
</label>
|
/>
|
||||||
</div>
|
<label class="!tw-font-normal" for="manageUsers">
|
||||||
<div>
|
{{ "manageUsers" | i18n }}
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
</div>
|
||||||
name="manageResetPassword"
|
<div>
|
||||||
id="manageResetPassword"
|
<input
|
||||||
formControlName="manageResetPassword"
|
type="checkbox"
|
||||||
(change)="handleDependentPermissions()"
|
name="manageResetPassword"
|
||||||
/>
|
id="manageResetPassword"
|
||||||
<label class="!tw-font-normal" for="manageResetPassword">
|
formControlName="manageResetPassword"
|
||||||
{{ "manageAccountRecovery" | i18n }}
|
(change)="handleDependentPermissions()"
|
||||||
</label>
|
/>
|
||||||
|
<label class="!tw-font-normal" for="manageResetPassword">
|
||||||
|
{{ "manageAccountRecovery" | i18n }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="canUseSecretsManager">
|
<ng-container *ngIf="canUseSecretsManager">
|
||||||
<h3 class="mt-4">
|
<h3 class="mt-4">
|
||||||
|
|
|
@ -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 { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
|
||||||
import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
|
@ -33,6 +35,7 @@ export class SecretsManagerSubscribeStandaloneComponent {
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private organizationService: InternalOrganizationServiceAbstraction,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
|
private configService: ConfigServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
@ -52,7 +55,11 @@ export class SecretsManagerSubscribeStandaloneComponent {
|
||||||
isMember: this.organization.isMember,
|
isMember: this.organization.isMember,
|
||||||
isProviderUser: this.organization.isProviderUser,
|
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
|
Because subscribing to Secrets Manager automatically provides access to Secrets Manager for the
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="editMode && organization?.canDeleteAssignedCollections"
|
*ngIf="canDelete$ | async"
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-trash"
|
bitIconButton="bwi-trash"
|
||||||
buttonType="danger"
|
buttonType="danger"
|
||||||
|
|
|
@ -313,6 +313,13 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||||
this.close(CollectionDialogAction.Deleted, this.collection);
|
this.close(CollectionDialogAction.Deleted, this.collection);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected canDelete$ = this.flexibleCollectionsEnabled$.pipe(
|
||||||
|
map(
|
||||||
|
(flexibleCollectionsEnabled) =>
|
||||||
|
this.editMode && this.collection.canDelete(this.organization, flexibleCollectionsEnabled),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
|
|
|
@ -106,7 +106,7 @@ export class VaultItemsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
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 {
|
protected canDeleteCollection(collection: CollectionView): boolean {
|
||||||
|
|
|
@ -31,15 +31,15 @@ export class CollectionAdminView extends CollectionView {
|
||||||
this.assigned = response.assigned;
|
this.assigned = response.assigned;
|
||||||
}
|
}
|
||||||
|
|
||||||
override canEdit(org: Organization): boolean {
|
override canEdit(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||||
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
return flexibleCollectionsEnabled
|
||||||
|
? org?.canEditAnyCollection
|
||||||
|
: org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
||||||
}
|
}
|
||||||
|
|
||||||
override canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
override canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||||
if (flexibleCollectionsEnabled) {
|
return flexibleCollectionsEnabled
|
||||||
return org?.canDeleteAnyCollection;
|
? org?.canDeleteAnyCollection
|
||||||
} else {
|
: org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
|
||||||
return org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { Component, Inject } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
@ -59,6 +61,7 @@ export class BulkDeleteDialogComponent {
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
|
private configService: ConfigServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
this.cipherIds = params.cipherIds ?? [];
|
this.cipherIds = params.cipherIds ?? [];
|
||||||
this.collectionIds = params.collectionIds ?? [];
|
this.collectionIds = params.collectionIds ?? [];
|
||||||
|
@ -125,11 +128,14 @@ export class BulkDeleteDialogComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteCollections(): Promise<any> {
|
private async deleteCollections(): Promise<any> {
|
||||||
|
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false,
|
||||||
|
);
|
||||||
// From org vault
|
// From org vault
|
||||||
if (this.organization) {
|
if (this.organization) {
|
||||||
if (
|
if (
|
||||||
!this.organization.canDeleteAssignedCollections &&
|
this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsEnabled))
|
||||||
!this.organization.canDeleteAnyCollection
|
|
||||||
) {
|
) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
|
@ -143,7 +149,8 @@ export class BulkDeleteDialogComponent {
|
||||||
} else if (this.organizations && this.collections) {
|
} else if (this.organizations && this.collections) {
|
||||||
const deletePromises: Promise<any>[] = [];
|
const deletePromises: Promise<any>[] = [];
|
||||||
for (const organization of this.organizations) {
|
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(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
|
@ -151,11 +158,9 @@ export class BulkDeleteDialogComponent {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const orgCollections = this.collections
|
const orgCollectionIds = orgCollections.map((c) => c.id);
|
||||||
.filter((o) => o.organizationId === organization.id)
|
|
||||||
.map((c) => c.id);
|
|
||||||
deletePromises.push(
|
deletePromises.push(
|
||||||
this.apiService.deleteManyCollections(this.organization.id, orgCollections),
|
this.apiService.deleteManyCollections(this.organization.id, orgCollectionIds),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await Promise.all(deletePromises);
|
return await Promise.all(deletePromises);
|
||||||
|
|
|
@ -146,7 +146,7 @@ export class VaultHeaderComponent {
|
||||||
const organization = this.organizations.find(
|
const organization = this.organizations.find(
|
||||||
(o) => o.id === this.collection?.node.organizationId,
|
(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> {
|
async editCollection(tab: CollectionDialogTabType): Promise<void> {
|
||||||
|
|
|
@ -688,7 +688,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||||
const organization = this.organizationService.get(collection.organizationId);
|
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(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
|
|
|
@ -152,7 +152,7 @@ export class VaultHeaderComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if we can edit the specified collection
|
// 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() {
|
addCipher() {
|
||||||
|
|
|
@ -132,6 +132,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
FeatureFlag.BulkCollectionAccess,
|
FeatureFlag.BulkCollectionAccess,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
protected flexibleCollectionsEnabled: boolean;
|
||||||
|
|
||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
|
@ -750,10 +751,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCollection(collection: CollectionView): Promise<void> {
|
async deleteCollection(collection: CollectionView): Promise<void> {
|
||||||
if (
|
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
!this.organization.canDeleteAssignedCollections &&
|
FeatureFlag.FlexibleCollections,
|
||||||
!this.organization.canDeleteAnyCollection
|
false,
|
||||||
) {
|
);
|
||||||
|
if (!collection.canDelete(this.organization, flexibleCollectionsEnabled)) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
|
|
|
@ -459,6 +459,7 @@ import { ModalService } from "./modal.service";
|
||||||
FolderApiServiceAbstraction,
|
FolderApiServiceAbstraction,
|
||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
SendApiServiceAbstraction,
|
SendApiServiceAbstraction,
|
||||||
|
ConfigServiceAbstraction,
|
||||||
LOGOUT_CALLBACK,
|
LOGOUT_CALLBACK,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -95,6 +95,12 @@ export abstract class OrganizationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
|
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
|
||||||
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
replace: (
|
||||||
upsert: (OrganizationData: OrganizationData | OrganizationData[]) => Promise<void>;
|
organizations: { [id: string]: OrganizationData },
|
||||||
|
flexibleCollectionsEnabled: boolean,
|
||||||
|
) => Promise<void>;
|
||||||
|
upsert: (
|
||||||
|
OrganizationData: OrganizationData | OrganizationData[],
|
||||||
|
flexibleCollectionsEnabled: boolean,
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,14 +186,29 @@ export class Organization {
|
||||||
return this.canEditAnyCollection || this.canDeleteAnyCollection;
|
return this.canEditAnyCollection || this.canDeleteAnyCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* This is deprecated with the introduction of Flexible Collections.
|
||||||
|
* This will always return false if FlexibleCollections flag is on.
|
||||||
|
*/
|
||||||
get canEditAssignedCollections() {
|
get canEditAssignedCollections() {
|
||||||
return this.isManager || this.permissions.editAssignedCollections;
|
return this.isManager || this.permissions.editAssignedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* This is deprecated with the introduction of Flexible Collections.
|
||||||
|
* This will always return false if FlexibleCollections flag is on.
|
||||||
|
*/
|
||||||
get canDeleteAssignedCollections() {
|
get canDeleteAssignedCollections() {
|
||||||
return this.isManager || this.permissions.deleteAssignedCollections;
|
return this.isManager || this.permissions.deleteAssignedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
* This is deprecated with the introduction of Flexible Collections.
|
||||||
|
* This will always return false if FlexibleCollections flag is on.
|
||||||
|
*/
|
||||||
get canViewAssignedCollections() {
|
get canViewAssignedCollections() {
|
||||||
return this.canDeleteAssignedCollections || this.canEditAssignedCollections;
|
return this.canDeleteAssignedCollections || this.canEditAssignedCollections;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ describe("Organization Service", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("upsert", async () => {
|
it("upsert", async () => {
|
||||||
await organizationService.upsert(organizationData("2", "Test 2"));
|
await organizationService.upsert(organizationData("2", "Test 2"), false);
|
||||||
|
|
||||||
expect(await firstValueFrom(organizationService.organizations$)).toEqual([
|
expect(await firstValueFrom(organizationService.organizations$)).toEqual([
|
||||||
{
|
{
|
||||||
|
@ -146,7 +146,7 @@ describe("Organization Service", () => {
|
||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
it("exists", async () => {
|
it("exists", async () => {
|
||||||
await organizationService.delete("1");
|
await organizationService.delete("1", false);
|
||||||
|
|
||||||
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
|
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ describe("Organization Service", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not exist", async () => {
|
it("does not exist", async () => {
|
||||||
organizationService.delete("1");
|
organizationService.delete("1", false);
|
||||||
|
|
||||||
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
|
expect(stateService.getOrganizations).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
InternalOrganizationServiceAbstraction,
|
InternalOrganizationServiceAbstraction,
|
||||||
isMember,
|
isMember,
|
||||||
} from "../../abstractions/organization/organization.service.abstraction";
|
} from "../../abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationUserType } from "../../enums";
|
||||||
import { OrganizationData } from "../../models/data/organization.data";
|
import { OrganizationData } from "../../models/data/organization.data";
|
||||||
import { Organization } from "../../models/domain/organization";
|
import { Organization } from "../../models/domain/organization";
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
||||||
return organizations.length > 0;
|
return organizations.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsert(organization: OrganizationData): Promise<void> {
|
async upsert(organization: OrganizationData, flexibleCollectionsEnabled: boolean): Promise<void> {
|
||||||
let organizations = await this.stateService.getOrganizations();
|
let organizations = await this.stateService.getOrganizations();
|
||||||
if (organizations == null) {
|
if (organizations == null) {
|
||||||
organizations = {};
|
organizations = {};
|
||||||
|
@ -59,10 +60,10 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
||||||
|
|
||||||
organizations[organization.id] = organization;
|
organizations[organization.id] = organization;
|
||||||
|
|
||||||
await this.replace(organizations);
|
await this.replace(organizations, flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string, flexibleCollectionsEnabled: boolean): Promise<void> {
|
||||||
const organizations = await this.stateService.getOrganizations();
|
const organizations = await this.stateService.getOrganizations();
|
||||||
if (organizations == null) {
|
if (organizations == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -73,7 +74,7 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
||||||
}
|
}
|
||||||
|
|
||||||
delete organizations[id];
|
delete organizations[id];
|
||||||
await this.replace(organizations);
|
await this.replace(organizations, flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id: string): Organization {
|
get(id: string): Organization {
|
||||||
|
@ -102,7 +103,24 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
|
||||||
return organizations.find((organization) => organization.identifier === identifier);
|
return organizations.find((organization) => organization.identifier === identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
async replace(organizations: { [id: string]: OrganizationData }) {
|
async replace(
|
||||||
|
organizations: { [id: string]: OrganizationData },
|
||||||
|
flexibleCollectionsEnabled: boolean,
|
||||||
|
) {
|
||||||
|
// If Flexible Collections is enabled, treat Managers as Users and ignore deprecated permissions
|
||||||
|
if (flexibleCollectionsEnabled) {
|
||||||
|
Object.values(organizations).forEach((o) => {
|
||||||
|
if (o.type === OrganizationUserType.Manager) {
|
||||||
|
o.type = OrganizationUserType.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.permissions != null) {
|
||||||
|
o.permissions.editAssignedCollections = false;
|
||||||
|
o.permissions.deleteAssignedCollections = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await this.stateService.setOrganizations(organizations);
|
await this.stateService.setOrganizations(organizations);
|
||||||
this.updateObservables(organizations);
|
this.updateObservables(organizations);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,13 +32,16 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For editing collection details, not the items within it.
|
// For editing collection details, not the items within it.
|
||||||
canEdit(org: Organization): boolean {
|
canEdit(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||||
if (org.id !== this.organizationId) {
|
if (org.id !== this.organizationId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Id of the organization provided does not match the org id of the collection.",
|
"Id of the organization provided does not match the org id of the collection.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return org?.canEditAnyCollection || org?.canEditAssignedCollections;
|
|
||||||
|
return flexibleCollectionsEnabled
|
||||||
|
? org?.canEditAnyCollection || this.manage
|
||||||
|
: org?.canEditAnyCollection || org?.canEditAssignedCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For deleting a collection, not the items within it.
|
// For deleting a collection, not the items within it.
|
||||||
|
@ -49,10 +52,8 @@ export class CollectionView implements View, ITreeNodeObject {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flexibleCollectionsEnabled) {
|
return flexibleCollectionsEnabled
|
||||||
return org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage);
|
? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage)
|
||||||
} else {
|
: org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||||
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||||
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
||||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||||
|
import { FeatureFlag } from "../../../enums/feature-flag.enum";
|
||||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||||
import {
|
import {
|
||||||
SyncCipherNotification,
|
SyncCipherNotification,
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
SyncSendNotification,
|
SyncSendNotification,
|
||||||
} from "../../../models/response/notification.response";
|
} from "../../../models/response/notification.response";
|
||||||
import { ProfileResponse } from "../../../models/response/profile.response";
|
import { ProfileResponse } from "../../../models/response/profile.response";
|
||||||
|
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { LogService } from "../../../platform/abstractions/log.service";
|
import { LogService } from "../../../platform/abstractions/log.service";
|
||||||
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
import { MessagingService } from "../../../platform/abstractions/messaging.service";
|
||||||
|
@ -59,6 +61,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||||
private folderApiService: FolderApiServiceAbstraction,
|
private folderApiService: FolderApiServiceAbstraction,
|
||||||
private organizationService: InternalOrganizationServiceAbstraction,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
private sendApiService: SendApiService,
|
private sendApiService: SendApiService,
|
||||||
|
private configService: ConfigServiceAbstraction,
|
||||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -318,7 +321,11 @@ export class SyncService implements SyncServiceAbstraction {
|
||||||
|
|
||||||
await this.setForceSetPasswordReasonIfNeeded(response);
|
await this.setForceSetPasswordReasonIfNeeded(response);
|
||||||
|
|
||||||
await this.syncProfileOrganizations(response);
|
const flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
await this.syncProfileOrganizations(response, flexibleCollectionsEnabled);
|
||||||
|
|
||||||
const providers: { [id: string]: ProviderData } = {};
|
const providers: { [id: string]: ProviderData } = {};
|
||||||
response.providers.forEach((p) => {
|
response.providers.forEach((p) => {
|
||||||
|
@ -374,7 +381,10 @@ export class SyncService implements SyncServiceAbstraction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncProfileOrganizations(response: ProfileResponse) {
|
private async syncProfileOrganizations(
|
||||||
|
response: ProfileResponse,
|
||||||
|
flexibleCollectionsEnabled: boolean,
|
||||||
|
) {
|
||||||
const organizations: { [id: string]: OrganizationData } = {};
|
const organizations: { [id: string]: OrganizationData } = {};
|
||||||
response.organizations.forEach((o) => {
|
response.organizations.forEach((o) => {
|
||||||
organizations[o.id] = new OrganizationData(o, {
|
organizations[o.id] = new OrganizationData(o, {
|
||||||
|
@ -394,7 +404,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.organizationService.replace(organizations);
|
await this.organizationService.replace(organizations, flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncFolders(response: FolderResponse[]) {
|
private async syncFolders(response: FolderResponse[]) {
|
||||||
|
|
Loading…
Reference in New Issue