1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

[AC-1373] Flexible Collections (#6336)

* [AC-1117] Add manage permission (#5910)

* Add 'manage' option to collection access permissions

* Add 'manage' to collection permissions

* remove service accidentally committed from another branch

* Update CLI commands

* update message casing to be consistent

* access selector model updates

* [AC-1374] Limit collection create/delete (#5963)

* feat: udate request/response/data/domain models for new column, refs AC-1374

* feat: create collection management ui, refs AC-1374

* fix: remove limitCollectionCdOwnerAdmin boolean from org update request, refs AC-1374

* fix: moved collection management UI, removed comments, refs AC-1374

* fix: observable chaining now properly calls API when local org updated, refs AC-1374

* fix: remove unused form template variables, refs AC-1374

* fix: clean up observable chain, refs AC-1374

* fix: remove parent.parent route, refs AC-1374

* fix: add cd explaination, refs AC-1374

* [AC-1649] Remove organizationId from collection-bulk-delete.request (#6343)

* refactor: remove organizationId from collection-bulk-delete-request, refs AC-1649

* refactor: remove request model from dialog component, refs AC-1649

* [AC-1174] Bulk collection management (#6133)

* [AC-1174] Add bulk edit collection access event type

* [AC-1174] Add bulk edit collection access menu option

* [AC-1174] Add initial bulk collections access dialog

* [AC-1174] Add logic to open bulk edit collections dialog

* [AC-1174] Move AccessItemView helper methods to access selector model to be shared

* [AC-1174] Add access selector to bulk collections dialog

* [AC-1174] Add bulk assign access method to collection-admin service

* [AC-1174] Introduce strongly typed BulkCollectionAccessRequest model

* [AC-1174] Update vault item event type name

* Update DialogService dependency

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion (#6409)

* Add manage property to synced Collection data

* Revert "Add manage property to synced Collection data"

Pushed to feature branch instead of a new one

This reverts commit 65cd39589c.

* Add manage property to synced Collection data

* Revert "Add manage property to synced Collection data"

This reverts commit f7fa30b79a.

* [AC-1680] Add manage property to collection view and response models (#6417)

* Add manage property to synced Collection data

* Update tests

* feat: add LimitCollectionCreationDeletion conditional to canCreateNewCollections logic, refs AC-1659 (#6429)

* [AC-1669] Enforce Can Manage permission on Collection dialog (#6493)

* [AC-1669] Cleanup unhandled promise warnings

* [AC-1669] Force change detection to ensure AccessSelector has the most recent items

* [AC-1669] Initially select acting member when creating a new collection

* [AC-1669] Add validator to ensure manage permission is selected

* [AC-1669] Update error toast logic to support access tab errors

* [AC-1669] Add error icon

* [AC-1713] [Flexible collections] Add feature flags to clients (#6486)

* Add FlexibleCollections and BulkCollectionAccess flags

* Flag Collection Management settings

* Flag bulk collection access dialog

* Flag collection access modal changes

* [AC-1662] Add LimitCollecitonCreationDeletion conditional to CanDelete logic (#6526)

* feat: implement limitCollectionCreationDeletion into canDelete logic, refs AC-1662

* feat: make canDelete functions backwards compatible with feature flag, refs AC-1662

* feat: update vault-items.component for async getter, refs AC-1662

* feat: update configService injection, refs AC-1662

* feat: add config service to canDelete reference, refs AC-1662

* fix: remove configservice dependency from views, refs AC-1757 (#6686)

* Add missing provider to vault-items.stories (#6690)

* Fix imports after update from master

---------

Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
Thomas Rittson
2023-11-01 19:30:59 +10:00
committed by GitHub
parent 2ec3f808d2
commit 0c3b569d0e
53 changed files with 725 additions and 138 deletions

View File

@@ -1,8 +1,10 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
import {
combineLatest,
firstValueFrom,
from,
map,
Observable,
of,
@@ -14,23 +16,27 @@ import {
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService, BitValidators } from "@bitwarden/components";
import { BitValidators, DialogService } from "@bitwarden/components";
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
import {
AccessItemView,
AccessItemValue,
AccessItemType,
convertToSelectionView,
AccessItemValue,
AccessItemView,
CollectionPermission,
convertToPermission,
convertToSelectionView,
mapGroupToAccessItemView,
mapUserToAccessItemView,
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { CollectionAdminService } from "../../core/collection-admin.service";
import { CollectionAdminView } from "../../core/views/collection-admin.view";
@@ -64,6 +70,11 @@ export enum CollectionDialogAction {
templateUrl: "collection-dialog.component.html",
})
export class CollectionDialogComponent implements OnInit, OnDestroy {
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollections,
false
);
private destroy$ = new Subject<void>();
protected organizations$: Observable<Organization[]>;
@@ -94,7 +105,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private organizationUserService: OrganizationUserService,
private dialogService: DialogService
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
private configService: ConfigServiceAbstraction
) {
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
}
@@ -118,7 +131,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
} else {
// Opened from the org vault
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
this.loadOrg(this.params.organizationId, this.params.collectionIds);
await this.loadOrg(this.params.organizationId, this.params.collectionIds);
}
if (await firstValueFrom(this.flexibleCollectionsEnabled$)) {
this.formGroup.controls.access.addValidators(validateCanManagePermission);
}
}
@@ -139,51 +156,76 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
organization: organization$,
collections: this.collectionService.getAll(orgId),
collectionDetails: this.params.collectionId
? this.collectionService.get(orgId, this.params.collectionId)
? from(this.collectionService.get(orgId, this.params.collectionId))
: of(null),
groups: groups$,
users: this.organizationUserService.getAllUsers(orgId),
flexibleCollections: this.flexibleCollectionsEnabled$,
})
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(({ organization, collections, collectionDetails, groups, users }) => {
this.organization = organization;
this.accessItems = [].concat(
groups.map(mapGroupToAccessItemView),
users.data.map(mapUserToAccessItemView)
);
.subscribe(
({ organization, collections, collectionDetails, groups, users, flexibleCollections }) => {
this.organization = organization;
this.accessItems = [].concat(
groups.map(mapGroupToAccessItemView),
users.data.map(mapUserToAccessItemView)
);
if (collectionIds) {
collections = collections.filter((c) => collectionIds.includes(c.id));
}
// Force change detection to update the access selector's items
this.changeDetectorRef.detectChanges();
if (this.params.collectionId) {
this.collection = collections.find((c) => c.id === this.collectionId);
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
if (collectionIds) {
collections = collections.filter((c) => collectionIds.includes(c.id));
}
const { name, parent } = parseName(this.collection);
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
this.deletedParentName = parent;
if (this.params.collectionId) {
this.collection = collections.find((c) => c.id === this.collectionId);
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
const { name, parent } = parseName(this.collection);
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
this.deletedParentName = parent;
}
const accessSelections = mapToAccessSelections(collectionDetails);
this.formGroup.patchValue({
name,
externalId: this.collection.externalId,
parent,
access: accessSelections,
});
} else {
this.nestOptions = collections;
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
const currentOrgUserId = users.data.find(
(u) => u.userId === this.organization?.userId
)?.id;
const initialSelection: AccessItemValue[] =
currentOrgUserId !== undefined
? [
{
id: currentOrgUserId,
type: AccessItemType.Member,
permission: flexibleCollections
? CollectionPermission.Manage
: CollectionPermission.Edit,
},
]
: [];
this.formGroup.patchValue({
parent: parent?.name ?? undefined,
access: initialSelection,
});
}
const accessSelections = mapToAccessSelections(collectionDetails);
this.formGroup.patchValue({
name,
externalId: this.collection.externalId,
parent,
access: accessSelections,
});
} else {
this.nestOptions = collections;
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
this.formGroup.patchValue({ parent: parent?.name ?? undefined });
this.loading = false;
}
this.loading = false;
});
);
}
protected get collectionId() {
@@ -202,12 +244,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
if (this.tabIndex === CollectionDialogTabType.Access) {
const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired");
if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
);
} else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access"))
);
}
return;
}
@@ -284,32 +334,6 @@ function parseName(collection: CollectionView) {
return { name, parent };
}
function mapGroupToAccessItemView(group: GroupView): AccessItemView {
return {
id: group.id,
type: AccessItemType.Group,
listName: group.name,
labelName: group.name,
accessAllItems: group.accessAll,
readonly: group.accessAll,
};
}
// TODO: Use view when user apis are migrated to a service
function mapUserToAccessItemView(user: OrganizationUserUserDetailsResponse): AccessItemView {
return {
id: user.id,
type: AccessItemType.Member,
email: user.email,
role: user.type,
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
labelName: user.name ?? user.email,
status: user.status,
accessAllItems: user.accessAll,
readonly: user.accessAll,
};
}
function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessItemValue[] {
if (collectionDetails == undefined) {
return [];
@@ -328,6 +352,16 @@ function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessIt
);
}
/**
* Validator to ensure that at least one access item has Manage permission
*/
function validateCanManagePermission(control: AbstractControl) {
const access = control.value as AccessItemValue[];
const hasManagePermission = access.some((a) => a.permission === CollectionPermission.Manage);
return hasManagePermission ? null : { managePermissionRequired: true };
}
/**
* Strongly typed helper to open a CollectionDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog