[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 305 additions and 118 deletions

View File

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

View File

@ -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(),
); );

View File

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

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 { 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -459,6 +459,7 @@ import { ModalService } from "./modal.service";
FolderApiServiceAbstraction, FolderApiServiceAbstraction,
OrganizationServiceAbstraction, OrganizationServiceAbstraction,
SendApiServiceAbstraction, SendApiServiceAbstraction,
ConfigServiceAbstraction,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
], ],
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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[]) {